app/build.gradle
@@ -90,6 +90,7 @@ dependencies { implementation project(':library') implementation project(':date_time_picker') implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.8.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' app/src/main/assets/js/map.js
@@ -170,6 +170,10 @@ if (lastClickedMarker !== null) { lastClickedMarker.setIcon(createIcon(CONFIG.IMAGES.MARKER_BLUE)); } if(lastClickedDivide!==null) { lastClickedDivide.setIcon(createIcon(CONFIG.IMAGES.DIVIDE_BLUE)); } if (isShowWaterIntakeDetail || isShowDivideDetail) { // 假如显示了取水口详情则隐藏取水口详情 isShowWaterIntakeDetail = false; app/src/main/java/com/dayu/pipirrapp/activity/OrderDealActivity.java
@@ -23,6 +23,7 @@ import com.dayu.pipirrapp.bean.net.AddProcessingRequest; import com.dayu.pipirrapp.bean.net.InsectionResult; import com.dayu.pipirrapp.bean.net.UplodFileState; import com.dayu.pipirrapp.dao.DaoSingleton; import com.dayu.pipirrapp.databinding.ActivityOrderDealBinding; import com.dayu.pipirrapp.fragment.OrderFragment; import com.dayu.pipirrapp.net.ApiManager; @@ -39,6 +40,8 @@ import com.dayu.pipirrapp.utils.ToastUtil; import com.dayu.pipirrapp.view.TitleBar; import com.jeremyliao.liveeventbus.LiveEventBus; import com.loper7.date_time_picker.DateTimeConfig; import com.loper7.date_time_picker.dialog.CardDatePickerDialog; import com.luck.picture.lib.basic.PictureSelectionModel; import com.luck.picture.lib.basic.PictureSelector; import com.luck.picture.lib.config.PictureMimeType; @@ -110,6 +113,27 @@ void initView() { new TitleBar(this).setTitleText("处理工单").setLeftIco().setLeftIcoListening(v -> OrderDealActivity.this.finish()); binding.timeLL.setOnClickListener(v -> { long time = System.currentTimeMillis(); List<Integer> list = new ArrayList<>(); list.add(DateTimeConfig.YEAR); list.add(DateTimeConfig.MONTH); list.add(DateTimeConfig.DAY); list.add(DateTimeConfig.HOUR); list.add(DateTimeConfig.MIN); new CardDatePickerDialog.Builder(this) .setTitle("选择处理时间") .setOnChoose("确定", aLong -> { //aLong = millisecond return null; }) .showBackNow(true) .setDefaultTime(time) .setMaxTime(time) .setDisplayType(list) .build().show(); }); mRecyclerView = binding.recycler; FullyGridLayoutManager manager = new FullyGridLayoutManager(this, 4, GridLayoutManager.VERTICAL, false); app/src/main/java/com/dayu/pipirrapp/activity/OrderDetailActivity.java
@@ -234,7 +234,10 @@ ImageInfo info = new ImageInfo(); info.setOriginUrl(imageResult.getWebPath()); info.setType(Type.IMAGE); info.setThumbnailUrl(imageResult.getWebPathZip()); if (imageResult.getWebPathZip()!=null){ info.setThumbnailUrl(imageResult.getWebPathZip()); } imageInfoList.add(info); } } @@ -248,7 +251,9 @@ ImageInfo info = new ImageInfo(); info.setOriginUrl(imageResult.getWebPath()); info.setThumbnailUrl(imageResult.getWebPath()); info.setThumbnailUrl(imageResult.getWebPathZip()); if (imageResult.getWebPathZip()!=null){ info.setThumbnailUrl(imageResult.getWebPathZip()); } info.setType(Type.VIDEO); imageInfoList.add(info); } app/src/main/res/layout/activity_order_deal.xml
@@ -33,8 +33,49 @@ android:orientation="vertical" android:padding="20dp"> <RelativeLayout android:id="@+id/timeLL" android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center" android:orientation="horizontal"> <TextView android:id="@+id/leftiocn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="* " android:gravity="center" android:layout_centerVertical="true" android:textColor="@color/base_red" android:textSize="@dimen/order_detail_button_size" /> <TextView android:id="@+id/timeTV" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toEndOf="@+id/leftiocn" android:text="反馈时间:" android:textColor="@color/black" android:textSize="@dimen/order_detail_button_size" /> <ImageView android:layout_width="25dp" android:layout_height="25dp" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:src="@drawable/ic_right" /> </RelativeLayout> <LinearLayout android:layout_width="match_parent" android:layout_marginTop="10dp" android:layout_height="wrap_content" android:orientation="horizontal"> date_time_picker/.gitignore
New file @@ -0,0 +1 @@ /build date_time_picker/build.gradle
New file @@ -0,0 +1,38 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' //apply plugin: 'kotlin-android-extensions' android { namespace 'com.loper7.date_time_picker' compileSdkVersion 29 buildToolsVersion "30.0.2" defaultConfig { minSdkVersion 21 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) compileOnly 'com.google.android.material:material:1.3.0' } date_time_picker/consumer-rules.pro
date_time_picker/proguard-rules.pro
New file @@ -0,0 +1,21 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile date_time_picker/src/androidTest/java/com/loper7/date_time_picker/ExampleInstrumentedTest.kt
New file @@ -0,0 +1,24 @@ package com.loper7.date_time_picker import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith import org.junit.Assert.* /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.loper7.date_time_picker.test", appContext.packageName) } } date_time_picker/src/main/AndroidManifest.xml
New file @@ -0,0 +1,2 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.loper7.date_time_picker" /> date_time_picker/src/main/java/com/loper7/date_time_picker/DateTimeConfig.kt
New file @@ -0,0 +1,75 @@ package com.loper7.date_time_picker import android.util.Log import com.loper7.date_time_picker.number_picker.NumberPicker import java.text.DateFormatSymbols import java.util.* import kotlin.collections.ArrayList /** * * @CreateDate: 2020/9/11 13:41 * @Description: java类作用描述 * @Author: LOPER7 * @Email: loper7@163.com */ object DateTimeConfig { const val YEAR = 0 const val MONTH = 1 const val DAY = 2 const val HOUR = 3 const val MIN = 4 const val SECOND = 5 const val GLOBAL_LOCAL = 0 const val GLOBAL_CHINA = 1 const val GLOBAL_US = 2 const val DATE_DEFAULT = 0 //公历 const val DATE_LUNAR = 1 //农历 //数字格式化,<10的数字前自动加0 val formatter = NumberPicker.Formatter { value: Int -> var str = value.toString() if (value < 10) { str = "0$str" } str } //国际化格月份格式化 val globalizationMonthFormatter = NumberPicker.Formatter { value: Int -> var str = value.toString() if (value in 1..12) str = DateFormatSymbols(Locale.US).months.toList()[value - 1] str } //国际化格月份格式化-缩写 val globalMonthFormatter = NumberPicker.Formatter { value: Int -> var str = value.toString() if (value in 1..12) { var month = DateFormatSymbols(Locale.US).months.toList()[value - 1] str = if (month.length > 3) month.substring(0, 3) else month } str } private fun isChina(): Boolean { return Locale.getDefault().language.contains("zh", true) } fun showChina(global: Int): Boolean { return global == GLOBAL_CHINA || (global == GLOBAL_LOCAL && isChina()) } } date_time_picker/src/main/java/com/loper7/date_time_picker/DateTimePicker.kt
New file @@ -0,0 +1,443 @@ package com.loper7.date_time_picker import android.content.Context import android.util.AttributeSet import android.view.View import android.widget.FrameLayout import androidx.annotation.ColorInt import androidx.annotation.Dimension import androidx.core.content.ContextCompat import com.loper7.date_time_picker.DateTimeConfig.DAY import com.loper7.date_time_picker.DateTimeConfig.GLOBAL_LOCAL import com.loper7.date_time_picker.DateTimeConfig.HOUR import com.loper7.date_time_picker.DateTimeConfig.MIN import com.loper7.date_time_picker.DateTimeConfig.MONTH import com.loper7.date_time_picker.DateTimeConfig.SECOND import com.loper7.date_time_picker.DateTimeConfig.YEAR import com.loper7.date_time_picker.controller.BaseDateTimeController import com.loper7.date_time_picker.controller.DateTimeInterface import com.loper7.date_time_picker.controller.DateTimeController import com.loper7.date_time_picker.number_picker.NumberPicker import com.loper7.tab_expand.ext.dip2px import com.loper7.tab_expand.ext.px2dip import org.jetbrains.annotations.NotNull import java.lang.Exception open class DateTimePicker : FrameLayout, DateTimeInterface { private var mYearSpinner: NumberPicker? = null private var mMonthSpinner: NumberPicker? = null private var mDaySpinner: NumberPicker? = null private var mHourSpinner: NumberPicker? = null private var mMinuteSpinner: NumberPicker? = null private var mSecondSpinner: NumberPicker? = null private var displayType = intArrayOf(YEAR, MONTH, DAY, HOUR, MIN, SECOND) private var showLabel = true private var themeColor = 0 private var textColor = 0 private var dividerColor = 0 private var selectTextSize = 0 private var normalTextSize = 0 private var yearLabel = "年" private var monthLabel = "月" private var dayLabel = "日" private var hourLabel = "时" private var minLabel = "分" private var secondLabel = "秒" private var global = GLOBAL_LOCAL private var layoutResId = R.layout.dt_layout_date_picker private var controller: BaseDateTimeController? = null private var textBold = true private var selectedTextBold = true constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : this(context, attrs) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { val attributesArray = context.obtainStyledAttributes(attrs, R.styleable.DateTimePicker) showLabel = attributesArray.getBoolean(R.styleable.DateTimePicker_dt_showLabel, true) themeColor = attributesArray.getColor( R.styleable.DateTimePicker_dt_themeColor, ContextCompat.getColor(context, R.color.colorAccent) ) textColor = attributesArray.getColor( R.styleable.DateTimePicker_dt_textColor, ContextCompat.getColor(context, R.color.colorTextGray) ) dividerColor= attributesArray.getColor( R.styleable.DateTimePicker_dt_dividerColor, ContextCompat.getColor(context, R.color.colorDivider) ) selectTextSize = context.px2dip( attributesArray.getDimensionPixelSize( R.styleable.DateTimePicker_dt_selectTextSize, context.dip2px(0f) ).toFloat() ) normalTextSize = context.px2dip( attributesArray.getDimensionPixelSize( R.styleable.DateTimePicker_dt_normalTextSize, context.dip2px(0f) ).toFloat() ) layoutResId = attributesArray.getResourceId( R.styleable.DateTimePicker_dt_layout, R.layout.dt_layout_date_picker ) textBold = attributesArray.getBoolean( R.styleable.DateTimePicker_dt_textBold, textBold ) selectedTextBold = attributesArray.getBoolean( R.styleable.DateTimePicker_dt_selectedTextBold, selectedTextBold ) attributesArray.recycle() init() } constructor(context: Context) : super(context) { init() } private fun init() { removeAllViews() try { if (!DateTimeConfig.showChina(global) && layoutResId == R.layout.dt_layout_date_picker) View.inflate(context, R.layout.dt_layout_date_picker_globalization, this) else View.inflate(context, layoutResId, this) } catch (e: Exception) { throw Exception("layoutResId is it right or not?") } mYearSpinner = findViewById(R.id.np_datetime_year) if (mYearSpinner == null) mYearSpinner = findViewWithTag("np_datetime_year") mMonthSpinner = findViewById(R.id.np_datetime_month) if (mMonthSpinner == null) mMonthSpinner = findViewWithTag("np_datetime_month") mDaySpinner = findViewById(R.id.np_datetime_day) if (mDaySpinner == null) mDaySpinner = findViewWithTag("np_datetime_day") mHourSpinner = findViewById(R.id.np_datetime_hour) if (mHourSpinner == null) mHourSpinner = findViewWithTag("np_datetime_hour") mMinuteSpinner = findViewById(R.id.np_datetime_minute) if (mMinuteSpinner == null) mMinuteSpinner = findViewWithTag("np_datetime_minute") mSecondSpinner = findViewById(R.id.np_datetime_second) if (mSecondSpinner == null) mSecondSpinner = findViewWithTag("np_datetime_second") setThemeColor(themeColor) setTextSize(normalTextSize, selectTextSize) showLabel(showLabel) setDisplayType(displayType) setSelectedTextBold(selectedTextBold) setTextBold(textBold) setTextColor(textColor) setDividerColor(dividerColor) //绑定控制器 bindController(controller ?: DateTimeController()) } /** * 绑定控制器 */ fun bindController(controller: BaseDateTimeController?) { this.controller = controller if (this.controller == null) this.controller = DateTimeController().bindPicker(YEAR, mYearSpinner) .bindPicker(MONTH, mMonthSpinner) .bindPicker(DAY, mDaySpinner).bindPicker(HOUR, mHourSpinner) .bindPicker(MIN, mMinuteSpinner).bindPicker(SECOND, mSecondSpinner) .bindGlobal(global)?.build() else this.controller?.bindPicker(YEAR, mYearSpinner) ?.bindPicker(MONTH, mMonthSpinner) ?.bindPicker(DAY, mDaySpinner)?.bindPicker(HOUR, mHourSpinner) ?.bindPicker(MIN, mMinuteSpinner)?.bindPicker(SECOND, mSecondSpinner) ?.bindGlobal(global)?.build() } /** * 设置国际化日期格式显示 * @param global : DateTimeConfig.GLOBAL_LOCAL * DateTimeConfig.GLOBAL_CHINA * DateTimeConfig.GLOBAL_US */ fun setGlobal(global: Int) { this.global = global init() } /** * 设置自定义layout */ fun setLayout(@NotNull layout: Int) { if (layout == 0) return layoutResId = layout init() } /** * 设置显示类型 * * @param types */ fun setDisplayType(types: IntArray?) { if (types == null || types.isEmpty()) return displayType = types if (!displayType.contains(YEAR)) { mYearSpinner?.visibility = View.GONE } if (!displayType.contains(MONTH)) { mMonthSpinner?.visibility = View.GONE } if (!displayType.contains(DAY)) { mDaySpinner?.visibility = View.GONE } if (!displayType.contains(HOUR)) { mHourSpinner?.visibility = View.GONE } if (!displayType.contains(MIN)) { mMinuteSpinner?.visibility = View.GONE } if (!displayType.contains(SECOND)) { mSecondSpinner?.visibility = View.GONE } } /** * 是否显示label * * @param b */ fun showLabel(b: Boolean) { showLabel = b if (b) { mYearSpinner?.label = yearLabel mMonthSpinner?.label = monthLabel mDaySpinner?.label = dayLabel mHourSpinner?.label = hourLabel mMinuteSpinner?.label = minLabel mSecondSpinner?.label = secondLabel } else { mYearSpinner?.label = "" mMonthSpinner?.label = "" mDaySpinner?.label = "" mHourSpinner?.label = "" mMinuteSpinner?.label = "" mSecondSpinner?.label = "" } } /** * 主题颜色 * * @param color */ fun setThemeColor(@ColorInt color: Int) { if (color == 0) return themeColor = color mYearSpinner?.selectedTextColor = themeColor mMonthSpinner?.selectedTextColor = themeColor mDaySpinner?.selectedTextColor = themeColor mHourSpinner?.selectedTextColor = themeColor mMinuteSpinner?.selectedTextColor = themeColor mSecondSpinner?.selectedTextColor = themeColor } /** * 设置选择器字体颜色 */ fun setTextColor(@ColorInt color: Int) { if (color == 0) return textColor = color mYearSpinner?.textColor = textColor mMonthSpinner?.textColor = textColor mDaySpinner?.textColor = textColor mHourSpinner?.textColor = textColor mMinuteSpinner?.textColor = textColor mSecondSpinner?.textColor = textColor } /** * 设置选择器分割线颜色 */ fun setDividerColor(@ColorInt color: Int) { if (color == 0) return dividerColor = color mYearSpinner?.dividerColor = color mMonthSpinner?.dividerColor = color mDaySpinner?.dividerColor = color mHourSpinner?.dividerColor = color mMinuteSpinner?.dividerColor = color mSecondSpinner?.dividerColor = color } /** * 字体大小 * * @param normal * @param select */ fun setTextSize(@Dimension normal: Int, @Dimension select: Int) { if (normal == 0) return if (select == 0) return var textSize = context!!.dip2px(select.toFloat()) var normalTextSize = context!!.dip2px(normal.toFloat()) mYearSpinner?.textSize = normalTextSize.toFloat() mMonthSpinner?.textSize = normalTextSize.toFloat() mDaySpinner?.textSize = normalTextSize.toFloat() mHourSpinner?.textSize = normalTextSize.toFloat() mMinuteSpinner?.textSize = normalTextSize.toFloat() mSecondSpinner?.textSize = normalTextSize.toFloat() mYearSpinner?.selectedTextSize = textSize.toFloat() mMonthSpinner?.selectedTextSize = textSize.toFloat() mDaySpinner?.selectedTextSize = textSize.toFloat() mHourSpinner?.selectedTextSize = textSize.toFloat() mMinuteSpinner?.selectedTextSize = textSize.toFloat() mSecondSpinner?.selectedTextSize = textSize.toFloat() } /** * 设置标签文字 * @param year 年标签 * @param month 月标签 * @param day 日标签 * @param hour 时标签 * @param min 分标签 * @param min 秒标签 */ fun setLabelText( year: String = yearLabel, month: String = monthLabel, day: String = dayLabel, hour: String = hourLabel, min: String = minLabel, second: String = secondLabel ) { this.yearLabel = year this.monthLabel = month this.dayLabel = day this.hourLabel = hour this.minLabel = min this.secondLabel = second showLabel(showLabel) } /** * 设置是否Picker循环滚动 * @param types 需要设置的Picker类型(DateTimeConfig-> YEAR,MONTH,DAY,HOUR,MIN,SECOND) * @param wrapSelector 是否循环滚动 */ fun setWrapSelectorWheel(vararg types: Int, wrapSelector: Boolean) { setWrapSelectorWheel(types.toMutableList(), wrapSelector) } /** * 设置是否Picker循环滚动 * @param wrapSelector 是否循环滚动 */ fun setWrapSelectorWheel(wrapSelector: Boolean) { setWrapSelectorWheel(null, wrapSelector) } /** * 获取类型对应的NumberPicker * @param displayType 类型 */ fun getPicker(displayType: Int): NumberPicker? { return when (displayType) { YEAR -> mYearSpinner MONTH -> mMonthSpinner DAY -> mDaySpinner HOUR -> mHourSpinner MIN -> mMinuteSpinner SECOND -> mSecondSpinner else -> null } } /** * 设置选择器字体是否加粗 */ fun setTextBold(textBold: Boolean) { this.textBold = textBold mYearSpinner?.isTextBold = textBold mMonthSpinner?.isTextBold = textBold mDaySpinner?.isTextBold = textBold mHourSpinner?.isTextBold = textBold mMinuteSpinner?.isTextBold = textBold mSecondSpinner?.isTextBold = textBold } /** * 设置选择器选中字体是否加粗 */ fun setSelectedTextBold(selectedTextBold: Boolean) { this.selectedTextBold = selectedTextBold mYearSpinner?.isSelectedTextBold = selectedTextBold mMonthSpinner?.isSelectedTextBold = selectedTextBold mDaySpinner?.isSelectedTextBold = selectedTextBold mHourSpinner?.isSelectedTextBold = selectedTextBold mMinuteSpinner?.isSelectedTextBold = selectedTextBold mSecondSpinner?.isSelectedTextBold = selectedTextBold } override fun setDefaultMillisecond(time: Long) { controller?.setDefaultMillisecond(time) } override fun setMinMillisecond(time: Long) { controller?.setMinMillisecond(time) } override fun setMaxMillisecond(time: Long) { controller?.setMaxMillisecond(time) } override fun setWrapSelectorWheel(types: MutableList<Int>?, wrapSelector: Boolean) { controller?.setWrapSelectorWheel(types, wrapSelector) } override fun setOnDateTimeChangedListener(callback: ((Long) -> Unit)?) { controller?.setOnDateTimeChangedListener(callback) } override fun getMillisecond(): Long { return controller?.getMillisecond() ?: 0L } } date_time_picker/src/main/java/com/loper7/date_time_picker/controller/BaseDateTimeController.kt
New file @@ -0,0 +1,43 @@ package com.loper7.date_time_picker.controller import com.loper7.date_time_picker.ext.getMaxDayInMonth import com.loper7.date_time_picker.number_picker.NumberPicker import java.lang.Exception import java.util.* /** * * @CreateDate: 2021/6/18 9:35 * @Description: 控制器基类 * @Author: LOPER7 * @Email: loper7@163.com */ abstract class BaseDateTimeController : DateTimeInterface { abstract fun bindPicker(type: Int, picker: NumberPicker?): BaseDateTimeController abstract fun bindGlobal(global: Int): BaseDateTimeController abstract fun build(): BaseDateTimeController /** * 获取某月最大天数 */ protected fun getMaxDayInMonth(year: Int?, month: Int?): Int { if (year == null || month == null) return 0 if (year <= 0 || month < 0) return 0 try { val calendar: Calendar = Calendar.getInstance() calendar.clear() calendar.set(Calendar.YEAR, year) calendar.set(Calendar.MONTH, month) return calendar.getMaxDayInMonth() } catch (e: Exception) { return 0 } } } date_time_picker/src/main/java/com/loper7/date_time_picker/controller/DateTimeController.kt
New file @@ -0,0 +1,320 @@ package com.loper7.date_time_picker.controller import android.util.Log import com.loper7.date_time_picker.DateTimeConfig import com.loper7.date_time_picker.DateTimeConfig.DAY import com.loper7.date_time_picker.DateTimeConfig.HOUR import com.loper7.date_time_picker.DateTimeConfig.MIN import com.loper7.date_time_picker.DateTimeConfig.MONTH import com.loper7.date_time_picker.DateTimeConfig.SECOND import com.loper7.date_time_picker.DateTimeConfig.YEAR import com.loper7.date_time_picker.ext.* import com.loper7.date_time_picker.ext.getMaxDayInMonth import com.loper7.date_time_picker.ext.isSameDay import com.loper7.date_time_picker.ext.isSameMonth import com.loper7.date_time_picker.ext.isSameYear import com.loper7.date_time_picker.number_picker.NumberPicker import com.loper7.date_time_picker.utils.StringUtils import java.util.* import kotlin.math.min /** * * @CreateDate: 2020/9/11 13:36 * @Description: 日期/时间逻辑控制器 * @Author: LOPER7 * @Email: loper7@163.com */ open class DateTimeController : BaseDateTimeController() { private var mYearSpinner: NumberPicker? = null private var mMonthSpinner: NumberPicker? = null private var mDaySpinner: NumberPicker? = null private var mHourSpinner: NumberPicker? = null private var mMinuteSpinner: NumberPicker? = null private var mSecondSpinner: NumberPicker? = null private lateinit var calendar: Calendar private lateinit var minCalendar: Calendar private lateinit var maxCalendar: Calendar private var global = DateTimeConfig.GLOBAL_LOCAL private var mOnDateTimeChangedListener: ((Long) -> Unit)? = null private var wrapSelectorWheel = true private var wrapSelectorWheelTypes: MutableList<Int>? = null override fun bindPicker(type: Int, picker: NumberPicker?): DateTimeController { when (type) { YEAR -> mYearSpinner = picker MONTH -> mMonthSpinner = picker DAY -> mDaySpinner = picker HOUR -> mHourSpinner = picker MIN -> mMinuteSpinner = picker SECOND -> mSecondSpinner = picker } return this } override fun bindGlobal(global: Int): DateTimeController { this.global = global return this } override fun build(): DateTimeController { calendar = Calendar.getInstance() calendar.set(Calendar.MILLISECOND,0) minCalendar = Calendar.getInstance() minCalendar.set(Calendar.YEAR, 1900) minCalendar.set(Calendar.MONTH, 0) minCalendar.set(Calendar.DAY_OF_MONTH, 1) minCalendar.set(Calendar.HOUR_OF_DAY, 0) minCalendar.set(Calendar.MINUTE, 0) minCalendar.set(Calendar.SECOND, 0) maxCalendar = Calendar.getInstance() maxCalendar.set(Calendar.YEAR, calendar.get(Calendar.YEAR) + 1900) maxCalendar.set(Calendar.MONTH, 11) maxCalendar.set(Calendar.DAY_OF_MONTH, maxCalendar.getMaxDayInMonth()) maxCalendar.set(Calendar.HOUR_OF_DAY, 23) maxCalendar.set(Calendar.MINUTE, 59) maxCalendar.set(Calendar.SECOND, 59) mYearSpinner?.run { maxValue = maxCalendar.get(Calendar.YEAR) minValue = minCalendar.get(Calendar.YEAR) value = calendar.get(Calendar.YEAR) isFocusable = true isFocusableInTouchMode = true descendantFocusability = NumberPicker.FOCUS_BLOCK_DESCENDANTS //设置NumberPicker不可编辑 setOnValueChangedListener(onChangeListener) } mMonthSpinner?.run { maxValue = maxCalendar.get(Calendar.MONTH) + 1 minValue = minCalendar.get(Calendar.MONTH) + 1 value = calendar.get(Calendar.MONTH) + 1 isFocusable = true isFocusableInTouchMode = true formatter = if (DateTimeConfig.showChina(global)) DateTimeConfig.formatter //格式化显示数字,个位数前添加0 else DateTimeConfig.globalMonthFormatter descendantFocusability = NumberPicker.FOCUS_BLOCK_DESCENDANTS setOnValueChangedListener(onChangeListener) } mDaySpinner?.run { maxValue = maxCalendar.get(Calendar.DAY_OF_MONTH) minValue = minCalendar.get(Calendar.DAY_OF_MONTH) value = calendar.get(Calendar.DAY_OF_MONTH) isFocusable = true isFocusableInTouchMode = true formatter = DateTimeConfig.formatter descendantFocusability = NumberPicker.FOCUS_BLOCK_DESCENDANTS setOnValueChangedListener(onChangeListener) } mHourSpinner?.run { maxValue = maxCalendar.get(Calendar.HOUR_OF_DAY) minValue = minCalendar.get(Calendar.HOUR_OF_DAY) isFocusable = true isFocusableInTouchMode = true value = calendar.get(Calendar.HOUR_OF_DAY) formatter = DateTimeConfig.formatter descendantFocusability = NumberPicker.FOCUS_BLOCK_DESCENDANTS setOnValueChangedListener(onChangeListener) } mMinuteSpinner?.run { maxValue = maxCalendar.get(Calendar.MINUTE) minValue = minCalendar.get(Calendar.MINUTE) isFocusable = true isFocusableInTouchMode = true value = calendar.get(Calendar.MINUTE) formatter = DateTimeConfig.formatter descendantFocusability = NumberPicker.FOCUS_BLOCK_DESCENDANTS setOnValueChangedListener(onChangeListener) } mSecondSpinner?.run { maxValue = maxCalendar.get(Calendar.SECOND) minValue = minCalendar.get(Calendar.SECOND) isFocusable = true isFocusableInTouchMode = true value = calendar.get(Calendar.SECOND) formatter = DateTimeConfig.formatter descendantFocusability = NumberPicker.FOCUS_BLOCK_DESCENDANTS setOnValueChangedListener(onChangeListener) } return this } private val onChangeListener = NumberPicker.OnValueChangeListener { view, old, new -> applyDateData() limitMaxAndMin() onDateTimeChanged() } /** * 同步数据 */ private fun applyDateData() { calendar.clear() mYearSpinner?.apply { calendar.set(Calendar.YEAR, value) } mMonthSpinner?.apply { calendar.set(Calendar.MONTH, (value - 1)) } var maxDayInMonth = getMaxDayInMonth(mYearSpinner?.value, (mMonthSpinner?.value ?: 0) - 1) if (mDaySpinner?.value ?: 0 >= maxDayInMonth) { mDaySpinner?.value = maxDayInMonth } mDaySpinner?.apply { calendar.set(Calendar.DAY_OF_MONTH, value) } mHourSpinner?.apply { calendar.set(Calendar.HOUR_OF_DAY, value) } mMinuteSpinner?.apply { calendar.set(Calendar.MINUTE, value) } mSecondSpinner?.apply { calendar.set(Calendar.SECOND, value) } } /** * 日期发生变化 */ private fun onDateTimeChanged() { if (mOnDateTimeChangedListener != null) { mOnDateTimeChangedListener?.invoke(calendar.timeInMillis) } } /** * 设置允许选择的区间 */ private fun limitMaxAndMin() { if(calendar.timeInMillis < minCalendar.timeInMillis){ calendar.clear() calendar.timeInMillis = minCalendar.timeInMillis } if(calendar.timeInMillis > maxCalendar.timeInMillis){ calendar.clear() calendar.timeInMillis = maxCalendar.timeInMillis } var maxDayInMonth = getMaxDayInMonth(calendar?.get(Calendar.YEAR), calendar?.get(Calendar.MONTH)) mMonthSpinner?.apply { minValue = if (calendar.isSameYear(minCalendar)) minCalendar.get(Calendar.MONTH) + 1 else 1 maxValue = if ((calendar.isSameYear(maxCalendar))) maxCalendar.get(Calendar.MONTH) + 1 else 12 } mDaySpinner?.apply { minValue = if (calendar.isSameMonth(minCalendar)) minCalendar.get(Calendar.DAY_OF_MONTH) else 1 maxValue = if (calendar.isSameMonth(maxCalendar)) maxCalendar.get(Calendar.DAY_OF_MONTH) else maxDayInMonth } mHourSpinner?.apply { minValue = if (calendar.isSameDay(minCalendar)) minCalendar.get(Calendar.HOUR_OF_DAY) else 0 maxValue = if (calendar.isSameDay(maxCalendar)) maxCalendar.get(Calendar.HOUR_OF_DAY) else 23 } mMinuteSpinner?.apply { minValue = if (calendar.isSameHour(minCalendar)) minCalendar.get(Calendar.MINUTE) else 0 maxValue = if (calendar.isSameHour(maxCalendar)) maxCalendar.get(Calendar.MINUTE) else 59 } mSecondSpinner?.apply { minValue = if (calendar.isSameMinute(minCalendar)) minCalendar.get(Calendar.SECOND) else 0 maxValue = if (calendar.isSameMinute(maxCalendar)) maxCalendar.get(Calendar.SECOND) else 59 } mYearSpinner?.value = calendar.get(Calendar.YEAR) mMonthSpinner?.value = calendar.get(Calendar.MONTH) + 1 mDaySpinner?.value = calendar.get(Calendar.DAY_OF_MONTH) mHourSpinner?.value = calendar.get(Calendar.HOUR_OF_DAY) mMinuteSpinner?.value = calendar.get(Calendar.MINUTE) mSecondSpinner?.value = calendar.get(Calendar.SECOND) if (mDaySpinner?.value ?: 0 >= maxDayInMonth) { mDaySpinner?.value = maxDayInMonth } setWrapSelectorWheel(wrapSelectorWheelTypes, wrapSelectorWheel) } override fun setDefaultMillisecond(time: Long) { if (time == 0L) return calendar.clear() calendar.timeInMillis = time limitMaxAndMin() onDateTimeChanged() } override fun setMinMillisecond(time: Long) { if (time == 0L) return if (maxCalendar?.timeInMillis in 1 until time) return if (minCalendar == null) minCalendar = Calendar.getInstance() minCalendar?.timeInMillis = time minCalendar?.let { mYearSpinner?.minValue = it.get(Calendar.YEAR) } setDefaultMillisecond(calendar.timeInMillis) } override fun setMaxMillisecond(time: Long) { if (time == 0L) return if (minCalendar?.timeInMillis!! > 0L && time < minCalendar?.timeInMillis!!) return if (maxCalendar == null) maxCalendar = Calendar.getInstance() maxCalendar?.timeInMillis = time mYearSpinner?.maxValue = maxCalendar?.get(Calendar.YEAR)!! setDefaultMillisecond(calendar.timeInMillis) } override fun setWrapSelectorWheel(types: MutableList<Int>?, wrapSelector: Boolean) { this.wrapSelectorWheelTypes = types this.wrapSelectorWheel = wrapSelector if (wrapSelectorWheelTypes == null || wrapSelectorWheelTypes!!.isEmpty()) { wrapSelectorWheelTypes = mutableListOf() wrapSelectorWheelTypes!!.add(YEAR) wrapSelectorWheelTypes!!.add(MONTH) wrapSelectorWheelTypes!!.add(DAY) wrapSelectorWheelTypes!!.add(HOUR) wrapSelectorWheelTypes!!.add(MIN) wrapSelectorWheelTypes!!.add(SECOND) } wrapSelectorWheelTypes!!.apply { for (type in this) { when (type) { YEAR -> mYearSpinner?.run { wrapSelectorWheel = wrapSelector } MONTH -> mMonthSpinner?.run { wrapSelectorWheel = wrapSelector } DAY -> mDaySpinner?.run { wrapSelectorWheel = wrapSelector } HOUR -> mHourSpinner?.run { wrapSelectorWheel = wrapSelector } MIN -> mMinuteSpinner?.run { wrapSelectorWheel = wrapSelector } SECOND -> mSecondSpinner?.run { wrapSelectorWheel = wrapSelector } } } } } override fun setOnDateTimeChangedListener(callback: ((Long) -> Unit)?) { mOnDateTimeChangedListener = callback onDateTimeChanged() } override fun getMillisecond(): Long { return calendar.timeInMillis } } date_time_picker/src/main/java/com/loper7/date_time_picker/controller/DateTimeInterface.kt
New file @@ -0,0 +1,51 @@ package com.loper7.date_time_picker.controller /** * * @CreateDate: 2020/9/11 16:55 * @Description: java类作用描述 * @Author: LOPER7 * @Email: loper7@163.com */ internal interface DateTimeInterface { /** * 设置默认时间戳 * * @param time */ fun setDefaultMillisecond(time:Long) /** * 设置最小选择时间 * * @param time */ fun setMinMillisecond(time: Long) /** * 设置最大选择时间 * * @param time */ fun setMaxMillisecond(time: Long) /** * 设置是否Picker循环滚动 * @param types 需要设置的Picker类型(DateTimeConfig-> YEAR,MONTH,DAY,HOUR,MIN,SECOND) * @param wrapSelector 是否循环滚动 */ fun setWrapSelectorWheel(types: MutableList<Int>?=null, wrapSelector: Boolean = true) /** * 选择回调监听 * @param long 选择时间戳 */ fun setOnDateTimeChangedListener(callback: ((Long) -> Unit)? = null) /** * 获取当前选中的时间戳 * @return long 当前选中的时间戳 */ fun getMillisecond():Long } date_time_picker/src/main/java/com/loper7/date_time_picker/dialog/CardDatePickerDialog.kt
New file @@ -0,0 +1,633 @@ package com.loper7.date_time_picker.dialog import android.annotation.SuppressLint import android.content.Context import android.graphics.Color import android.graphics.drawable.GradientDrawable import android.os.Bundle import android.text.Html import android.util.Log import android.view.View import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.loper7.date_time_picker.DateTimeConfig import com.loper7.date_time_picker.DateTimeConfig.DATE_DEFAULT import com.loper7.date_time_picker.DateTimeConfig.DATE_LUNAR import com.loper7.date_time_picker.DateTimePicker import com.loper7.date_time_picker.R import com.loper7.date_time_picker.utils.StringUtils import com.loper7.date_time_picker.utils.lunar.Lunar import org.jetbrains.annotations.NotNull import java.util.* /** * * @ProjectName: DatePicker * @Package: com.loper7.date_time_picker.dialog * @ClassName: DateDateDateTimePickerDialog * @CreateDate: 2020/3/3 0003 11:38 * @Description: * @Author: LOPER7 * @Email: loper7@163.com */ open class CardDatePickerDialog(context: Context) : BottomSheetDialog(context, R.style.DateTimePicker_BottomSheetDialog), View.OnClickListener { companion object { const val CARD = 0 //卡片 const val CUBE = 1 //方形 const val STACK = 2 //顶部圆角 fun builder(context: Context): Builder { return lazy { Builder(context) }.value } } private var builder: Builder? = null private var tv_cancel: TextView? = null private var tv_submit: TextView? = null private var tv_title: TextView? = null private var tv_choose_date: TextView? = null private var btn_today: TextView? = null private var datePicker: DateTimePicker? = null private var tv_go_back: TextView? = null private var linear_now: LinearLayout? = null private var linear_bg: LinearLayout? = null private var mBehavior: BottomSheetBehavior<FrameLayout>? = null private var divider_top:View?=null private var divider_bottom:View?=null private var divider_line:View?=null private var millisecond: Long = 0 constructor(context: Context, builder: Builder) : this(context) { this.builder = builder } init { builder = builder(context) } @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { setContentView(R.layout.dt_dialog_time_picker) super.onCreate(savedInstanceState) val bottomSheet = delegate.findViewById<FrameLayout>(R.id.design_bottom_sheet) bottomSheet!!.setBackgroundColor(Color.TRANSPARENT) tv_cancel = findViewById(R.id.dialog_cancel) tv_submit = findViewById(R.id.dialog_submit) datePicker = findViewById(R.id.dateTimePicker) tv_title = findViewById(R.id.tv_title) btn_today = findViewById(R.id.btn_today) tv_choose_date = findViewById(R.id.tv_choose_date) tv_go_back = findViewById(R.id.tv_go_back) linear_now = findViewById(R.id.linear_now) linear_bg = findViewById(R.id.linear_bg) divider_top = findViewById(R.id.divider_top) divider_bottom = findViewById(R.id.divider_bottom) divider_line = findViewById(R.id.dialog_select_border) mBehavior = BottomSheetBehavior.from(bottomSheet) //滑动关闭 mBehavior?.isHideable = builder?.touchHideable ?: true //背景模式 if (builder!!.model != 0) { val parmas = LinearLayout.LayoutParams(linear_bg!!.layoutParams) when (builder!!.model) { CARD -> { parmas.setMargins(dip2px(12f), dip2px(12f), dip2px(12f), dip2px(12f)) linear_bg!!.layoutParams = parmas linear_bg!!.setBackgroundResource(R.drawable.shape_bg_round_white_5) } CUBE -> { parmas.setMargins(0, 0, 0, 0) linear_bg!!.layoutParams = parmas linear_bg!!.setBackgroundColor( ContextCompat.getColor( context, R.color.colorTextWhite ) ) } STACK -> { parmas.setMargins(0, 0, 0, 0) linear_bg!!.layoutParams = parmas linear_bg!!.setBackgroundResource(R.drawable.shape_bg_top_round_white_15) } else -> { parmas.setMargins(0, 0, 0, 0) linear_bg!!.layoutParams = parmas linear_bg!!.setBackgroundResource(builder!!.model) } } } //标题 if (builder!!.titleValue.isNullOrEmpty()) { tv_title!!.visibility = View.GONE } else { tv_title?.text = builder!!.titleValue tv_title?.visibility = View.VISIBLE } //按钮 tv_cancel?.text = builder!!.cancelText tv_submit?.text = builder!!.chooseText //设置自定义layout datePicker!!.setLayout(builder!!.pickerLayoutResId) //显示标签 datePicker!!.showLabel(builder!!.dateLabel) //设置标签文字 datePicker!!.setLabelText( builder!!.yearLabel, builder!!.monthLabel, builder!!.dayLabel, builder!!.hourLabel, builder!!.minLabel, builder!!.secondLabel ) //显示模式 if (builder!!.displayTypes == null) { builder!!.displayTypes = intArrayOf( DateTimeConfig.YEAR, DateTimeConfig.MONTH, DateTimeConfig.DAY, DateTimeConfig.HOUR, DateTimeConfig.MIN, DateTimeConfig.SECOND ) } datePicker!!.setDisplayType(builder!!.displayTypes) //回到当前时间展示 if (builder!!.displayTypes != null) { var year_month_day_hour = 0 for (i in builder!!.displayTypes!!) { if (i == DateTimeConfig.YEAR && year_month_day_hour <= 0) { year_month_day_hour = 0 tv_go_back!!.text = "回到今年" btn_today!!.text = "今" } if (i == DateTimeConfig.MONTH && year_month_day_hour <= 1) { year_month_day_hour = 1 tv_go_back!!.text = "回到本月" btn_today!!.text = "本" } if (i == DateTimeConfig.DAY && year_month_day_hour <= 2) { year_month_day_hour = 2 tv_go_back!!.text = "回到今日" btn_today!!.text = "今" } if ((i == DateTimeConfig.HOUR || i == DateTimeConfig.MIN) && year_month_day_hour <= 3) { year_month_day_hour = 3 tv_go_back!!.text = "回到此刻" btn_today!!.text = "此" } } } linear_now!!.visibility = if (builder!!.backNow) View.VISIBLE else View.GONE tv_choose_date!!.visibility = if (builder!!.focusDateInfo) View.VISIBLE else View.GONE //强制关闭国际化(不受系统语言影响) datePicker!!.setGlobal(DateTimeConfig.GLOBAL_CHINA) //设置最小时间 datePicker!!.setMinMillisecond(builder!!.minTime) //设置最大时间 datePicker!!.setMaxMillisecond(builder!!.maxTime) //设置默认时间 datePicker!!.setDefaultMillisecond(builder!!.defaultMillisecond) //设置是否循环滚动 datePicker!!.setWrapSelectorWheel( builder!!.wrapSelectorWheelTypes, builder!!.wrapSelectorWheel ) datePicker!!.setTextSize(13, 15) if (builder!!.themeColor != 0) { datePicker!!.setThemeColor(builder!!.themeColor) tv_submit!!.setTextColor(builder!!.themeColor) val gd = GradientDrawable() gd.setColor(builder!!.themeColor) gd.cornerRadius = dip2px(60f).toFloat() btn_today!!.background = gd } if (builder!!.assistColor != 0) { tv_title?.setTextColor(builder!!.assistColor) tv_choose_date?.setTextColor(builder!!.assistColor) tv_go_back?.setTextColor(builder!!.assistColor) tv_cancel?.setTextColor(builder!!.assistColor) datePicker!!.setTextColor(builder!!.assistColor) } if (builder!!.dividerColor != 0) { divider_top?.setBackgroundColor(builder!!.dividerColor) divider_bottom?.setBackgroundColor(builder!!.dividerColor) divider_line?.setBackgroundColor(builder!!.dividerColor) datePicker!!.setDividerColor(builder!!.dividerColor) } tv_cancel!!.setOnClickListener(this) tv_submit!!.setOnClickListener(this) btn_today!!.setOnClickListener(this) datePicker!!.setOnDateTimeChangedListener { millisecond -> this@CardDatePickerDialog.millisecond = millisecond var calendar = Calendar.getInstance() calendar.clear() calendar.timeInMillis = millisecond when (builder?.chooseDateModel) { DATE_LUNAR -> { Lunar.getInstance(calendar).apply { var str = if (this == null) "暂无农历信息" else "农历 $yearName$monthName$dayName ${StringUtils.getWeek(millisecond)}" tv_choose_date?.text = Html.fromHtml(str) } } else -> tv_choose_date?.text = StringUtils.conversionTime(millisecond, "yyyy年MM月dd日 ") + StringUtils.getWeek( millisecond ) } } } override fun onStart() { super.onStart() mBehavior?.state = BottomSheetBehavior.STATE_EXPANDED } override fun onClick(v: View) { this.dismiss() when (v.id) { R.id.btn_today -> { builder?.onChooseListener?.invoke(Calendar.getInstance().timeInMillis) } R.id.dialog_submit -> { builder?.onChooseListener?.invoke(millisecond) } R.id.dialog_cancel -> { builder?.onCancelListener?.invoke() } } this.dismiss() } class Builder(private var context: Context) { @JvmField var backNow: Boolean = true @JvmField var focusDateInfo: Boolean = true @JvmField var dateLabel: Boolean = true @JvmField var cancelText: String = "取消" @JvmField var chooseText: String = "确定" @JvmField var titleValue: String? = null @JvmField var defaultMillisecond: Long = 0 @JvmField var minTime: Long = 0 @JvmField var maxTime: Long = 0 @JvmField var displayTypes: IntArray? = null @JvmField var model: Int = CARD @JvmField var themeColor: Int = 0 @JvmField var assistColor: Int = 0 @JvmField var dividerColor: Int = 0 @JvmField var pickerLayoutResId: Int = 0 @JvmField var wrapSelectorWheel: Boolean = true @JvmField var wrapSelectorWheelTypes: MutableList<Int>? = mutableListOf() @JvmField var touchHideable: Boolean = true @JvmField var chooseDateModel: Int = DATE_DEFAULT @JvmField var onChooseListener: ((Long) -> Unit)? = null @JvmField var onCancelListener: (() -> Unit)? = null var yearLabel = "年" var monthLabel = "月" var dayLabel = "日" var hourLabel = "时" var minLabel = "分" var secondLabel = "秒" /** * 设置标题 * @param value 标题 * @return Builder */ fun setTitle(value: String): Builder { this.titleValue = value return this } /** * 设置显示值 * @param types 要显示的年月日时分秒标签 * @return Builder */ fun setDisplayType(vararg types: Int): Builder { this.displayTypes = types return this } /** * 设置显示值 * @param types 要显示的年月日时分秒标签 * @return Builder */ fun setDisplayType(types: MutableList<Int>?): Builder { this.displayTypes = types?.toIntArray() return this } /** * 设置默认时间 * @param millisecond 默认时间 * @return Builder */ fun setDefaultTime(millisecond: Long): Builder { this.defaultMillisecond = millisecond return this } /** * 设置范围最小值 * @param millisecond 范围最小值 * @return Builder */ fun setMinTime(millisecond: Long): Builder { this.minTime = millisecond return this } /** * 设置范围最大值 * @param millisecond * @return Builder */ fun setMaxTime(millisecond: Long): Builder { this.maxTime = millisecond return this } /** * 是否显示回到当前 * @param b 是否显示回到当前 * @return Builder */ fun showBackNow(b: Boolean): Builder { this.backNow = b return this } /** * 是否显示选中日期信息 * @param b 是否显示选中日期信息 * @return Builder */ fun showFocusDateInfo(b: Boolean): Builder { this.focusDateInfo = b return this } /** * 是否显示单位标签 * @param b 是否显示单位标签 * @return Builder */ fun showDateLabel(b: Boolean): Builder { this.dateLabel = b return this } /** * 显示模式 * @param model CARD,CUBE,STACK * @return Builder */ fun setBackGroundModel(model: Int): Builder { this.model = model return this } /** * 设置主题颜色 * @param themeColor 主题颜色 * @return Builder */ fun setThemeColor(@ColorInt themeColor: Int): Builder { this.themeColor = themeColor return this } /** * 设置标签文字 * @param year 年标签 * @param month 月标签 * @param day 日标签 * @param hour 时标签 * @param min 分标签 * @param second 秒标签 *setLabelText("年","月","日","时") *setLabelText(month="月",hour="时") * @return Builder */ fun setLabelText( year: String = yearLabel, month: String = monthLabel, day: String = dayLabel, hour: String = hourLabel, min: String = minLabel, second: String = secondLabel ): Builder { this.yearLabel = year this.monthLabel = month this.dayLabel = day this.hourLabel = hour this.minLabel = min this.secondLabel = second return this } /** *设置是否循环滚动 *{@link #setWrapSelectorWheel()} * @return Builder */ fun setWrapSelectorWheel(vararg types: Int, wrapSelector: Boolean): Builder { return setWrapSelectorWheel(types.toMutableList(), wrapSelector) } /** * 设置是否循环滚动 * @param wrapSelector * @return Builder */ fun setWrapSelectorWheel(wrapSelector: Boolean): Builder { return setWrapSelectorWheel(null, wrapSelector) } /** * 设置是否循环滚动 * @param types 需要设置的标签项 * @param wrapSelector 是否循环滚动 * @return Builder */ fun setWrapSelectorWheel(types: MutableList<Int>?, wrapSelector: Boolean): Builder { this.wrapSelectorWheelTypes = types this.wrapSelectorWheel = wrapSelector return this } /** * 绑定选择监听 * @param text 按钮文字 * @param listener 选择监听函数 long 时间戳 * @return Builder */ fun setOnChoose(text: String = "确定", listener: ((Long) -> Unit)? = null): Builder { this.onChooseListener = listener this.chooseText = text return this } /** * 绑定取消监听 * @param text 按钮文字 * @param listener 取消监听函数 * @return Builder */ fun setOnCancel(text: String = "取消", listener: (() -> Unit)? = null): Builder { this.onCancelListener = listener this.cancelText = text return this } /** * 设置自定义选择器layout * @param layoutResId xml资源id */ fun setPickerLayout(@NotNull layoutResId: Int): Builder { this.pickerLayoutResId = layoutResId return this } /** * 是否可以滑动关闭弹窗 * @param touchHideable 默认为 true */ fun setTouchHideable(touchHideable: Boolean = true): Builder { this.touchHideable = touchHideable return this } /** * 设置dialog选中日期信息展示格式 * @param value 1:LUNAR 0:DEFAULT * @return Builder */ fun setChooseDateModel(value: Int): Builder { this.chooseDateModel = value return this } /** * 这只dialog内辅助文字的颜色 * @return Builder */ fun setAssistColor(@ColorInt value: Int): Builder { this.assistColor = value return this } /** * 这只dialog内分割线颜色 * @return Builder */ fun setDividerColor(@ColorInt value: Int): Builder { this.dividerColor = value return this } fun build(): CardDatePickerDialog { return CardDatePickerDialog(context, this) } } /** * 根据手机的分辨率dp 转成px(像素) */ private fun dip2px(dpValue: Float): Int { val scale = context.resources.displayMetrics.density return (dpValue * scale + 0.5f).toInt() } /** * 根据手机的分辨率px(像素) 转成dp */ private fun px2dip(pxValue: Float): Int { val scale = context.resources.displayMetrics.density return (pxValue / scale + 0.5f).toInt() } } date_time_picker/src/main/java/com/loper7/date_time_picker/dialog/CardWeekPickerDialog.kt
New file @@ -0,0 +1,379 @@ package com.loper7.date_time_picker.dialog import android.content.Context import android.graphics.Color import android.os.Bundle import android.util.Log import android.view.View import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.loper7.date_time_picker.R import com.loper7.date_time_picker.ext.* import com.loper7.date_time_picker.ext.getMaxWeekOfYear import com.loper7.date_time_picker.ext.getWeekOfYear import com.loper7.date_time_picker.ext.toFormatList import com.loper7.date_time_picker.number_picker.NumberPicker import com.loper7.date_time_picker.utils.StringUtils import com.loper7.tab_expand.ext.dip2px import java.util.* /** * 卡片 周视图 选择器 */ open class CardWeekPickerDialog(context: Context) : BottomSheetDialog(context), View.OnClickListener { companion object { const val CARD = 0 //卡片 const val CUBE = 1 //方形 const val STACK = 2 //顶部圆角 fun builder(context: Context): Builder { return lazy { Builder(context) }.value } } private var builder: Builder? = null private val np_week by lazy { delegate.findViewById<NumberPicker>(R.id.np_week) } private val tv_cancel by lazy { delegate.findViewById<TextView>(R.id.dialog_cancel) } private val tv_submit by lazy { delegate.findViewById<TextView>(R.id.dialog_submit) } private val tv_title by lazy { delegate.findViewById<TextView>(R.id.tv_title) } private val linear_bg by lazy { delegate.findViewById<LinearLayout>(R.id.linear_bg) } private val divider_bottom by lazy { delegate.findViewById<View>(R.id.divider_bottom) } private val divider_line by lazy { delegate.findViewById<View>(R.id.dialog_select_border) } private var mBehavior: BottomSheetBehavior<FrameLayout>? = null private val calendar by lazy { Calendar.getInstance() } private var weeksData = mutableListOf<MutableList<Long>>() constructor(context: Context, builder: Builder) : this(context) { this.builder = builder } init { builder = builder(context) } override fun onCreate(savedInstanceState: Bundle?) { setContentView(R.layout.dt_dialog_week_picker) super.onCreate(savedInstanceState) val bottomSheet = delegate.findViewById<FrameLayout>(R.id.design_bottom_sheet) bottomSheet!!.setBackgroundColor(Color.TRANSPARENT) mBehavior = BottomSheetBehavior.from(bottomSheet) weeksData = calendar.getWeeks() builder?.apply { weeksData = calendar.getWeeks(startMillisecond, endMillisecond, startContain, endContain) //背景模式 if (model != 0) { val parmas = LinearLayout.LayoutParams(linear_bg!!.layoutParams) when (model) { CARD -> { parmas.setMargins( context.dip2px(12f), context.dip2px(12f), context.dip2px(12f), context.dip2px(12f) ) linear_bg!!.layoutParams = parmas linear_bg!!.setBackgroundResource(R.drawable.shape_bg_round_white_5) } CUBE -> { parmas.setMargins(0, 0, 0, 0) linear_bg!!.layoutParams = parmas linear_bg!!.setBackgroundColor( ContextCompat.getColor( context, R.color.colorTextWhite ) ) } STACK -> { parmas.setMargins(0, 0, 0, 0) linear_bg!!.layoutParams = parmas linear_bg!!.setBackgroundResource(R.drawable.shape_bg_top_round_white_15) } else -> { parmas.setMargins(0, 0, 0, 0) linear_bg!!.layoutParams = parmas linear_bg!!.setBackgroundResource(model) } } } //标题 if (titleValue.isNullOrEmpty()) { tv_title!!.visibility = View.GONE } else { tv_title?.text = titleValue tv_title?.visibility = View.VISIBLE } //按钮 tv_cancel?.text = cancelText tv_submit?.text = chooseText //主题 if (themeColor != 0) { tv_submit!!.setTextColor(themeColor) np_week!!.selectedTextColor = themeColor } if (builder!!.assistColor != 0) { tv_title?.setTextColor(builder!!.assistColor) tv_cancel?.setTextColor(builder!!.assistColor) np_week!!.textColor = builder!!.assistColor } if (builder!!.dividerColor != 0) { divider_bottom?.setBackgroundColor(builder!!.dividerColor) divider_line?.setBackgroundColor(builder!!.dividerColor) np_week!!.dividerColor = builder!!.dividerColor } } //视图周 np_week?.apply { if (weeksData.isNullOrEmpty()) return minValue = 1 maxValue = weeksData.size value = weeksData.index(builder?.defaultMillisecond) + 1 isFocusable = true isFocusableInTouchMode = true descendantFocusability = NumberPicker.FOCUS_BLOCK_DESCENDANTS //设置NumberPicker不可编辑 wrapSelectorWheel = builder?.wrapSelectorWheel ?: true formatter = builder?.formatter?.invoke(weeksData) ?: NumberPicker.Formatter { value: Int -> var weekData = weeksData[value - 1].toFormatList("yyyy/MM/dd") var str = "${weekData.first()} - ${weekData.last()}" str } } tv_cancel!!.setOnClickListener(this) tv_submit!!.setOnClickListener(this) } override fun onStart() { super.onStart() mBehavior?.state = BottomSheetBehavior.STATE_EXPANDED } override fun onClick(v: View) { this.dismiss() when (v.id) { R.id.dialog_submit -> { np_week?.apply { builder?.onChooseListener?.invoke(weeksData[value - 1], formatter.format(value)) } } R.id.dialog_cancel -> { builder?.onCancelListener?.invoke() } } this.dismiss() } class Builder(private var context: Context) { @JvmField var cancelText: String = "取消" @JvmField var chooseText: String = "确定" @JvmField var titleValue: String? = null @JvmField var model: Int = CARD @JvmField var themeColor: Int = 0 @JvmField var assistColor: Int = 0 @JvmField var dividerColor: Int = 0 @JvmField var wrapSelectorWheel: Boolean = true @JvmField var onChooseListener: ((MutableList<Long>, String) -> Unit)? = null @JvmField var onCancelListener: (() -> Unit)? = null @JvmField var defaultMillisecond: Long = 0 @JvmField var startMillisecond: Long = 0 @JvmField var startContain: Boolean = true @JvmField var endMillisecond: Long = 0 @JvmField var endContain: Boolean = true @JvmField var formatter: ((MutableList<MutableList<Long>>) -> NumberPicker.Formatter?)? = null /** * 设置标题 * @param value 标题 * @return Builder */ fun setTitle(value: String): Builder { this.titleValue = value return this } /** * 显示模式 * @param model CARD,CUBE,STACK * @return Builder */ fun setBackGroundModel(model: Int): Builder { this.model = model return this } /** * 设置主题颜色 * @param themeColor 主题颜色 * @return Builder */ fun setThemeColor(@ColorInt themeColor: Int): Builder { this.themeColor = themeColor return this } /** *设置是否循环滚动 * @return Builder */ fun setWrapSelectorWheel(wrapSelector: Boolean): Builder { this.wrapSelectorWheel = wrapSelector return this } /** * 设置默认选中周次所在的任意时间 * @param millisecond 默认时间 * @return Builder */ fun setDefaultMillisecond(millisecond: Long): Builder { this.defaultMillisecond = millisecond return this } /** * 设置起始周所在时间 * @param millisecond 起始时间 * @param contain 起始周是否包含起始时间 * @return Builder */ fun setStartMillisecond(millisecond: Long, contain: Boolean = true): Builder { this.startMillisecond = millisecond this.startContain = contain return this } /** * 设置结束周所在时间 * @param millisecond 结束时间 * @param contain 结束周是否包含结束时间 * @return Builder */ fun setEndMillisecond(millisecond: Long, contain: Boolean = true): Builder { this.endMillisecond = millisecond this.endContain = contain return this } /** * 设置格式化 * @param datas 数据 * @return Builder */ fun setFormatter(formatter: (MutableList<MutableList<Long>>) -> NumberPicker.Formatter?): Builder { this.formatter = formatter return this } /** * 绑定选择监听 * @param text 按钮文字 * @param listener 选择监听函数 MutableList<Long> 选择周次所包含的天时间戳 String 周format字符串 * @return Builder */ fun setOnChoose( text: String = "确定", listener: ((MutableList<Long>, String) -> Unit)? = null ): Builder { this.onChooseListener = listener this.chooseText = text return this } /** * 绑定取消监听 * @param text 按钮文字 * @param listener 取消监听函数 * @return Builder */ fun setOnCancel(text: String = "取消", listener: (() -> Unit)? = null): Builder { this.onCancelListener = listener this.cancelText = text return this } /** * 这只dialog内辅助文字的颜色 * @return Builder */ fun setAssistColor(@ColorInt value: Int): Builder { this.assistColor = value return this } /** * 这只dialog内分割线颜色 * @return Builder */ fun setDividerColor(@ColorInt value: Int): Builder { this.dividerColor = value return this } fun build(): CardWeekPickerDialog { return CardWeekPickerDialog(context, this) } } } date_time_picker/src/main/java/com/loper7/date_time_picker/ext/CalendarExt.kt
New file @@ -0,0 +1,200 @@ package com.loper7.date_time_picker.ext import android.util.Log import java.lang.RuntimeException import java.time.Year import java.util.* /** * 获取一年中所有的周 * @param year 1900-9999 default:now * @return MutableList<MutableList<Long>> */ internal fun Calendar.getWeeksOfYear( year: Int = Calendar.getInstance().get(Calendar.YEAR) ): MutableList<MutableList<Long>> { if (year < 1900 || year > 9999) throw NullPointerException("The year must be within 1900-9999") firstDayOfWeek = Calendar.MONDAY set(Calendar.DAY_OF_WEEK, Calendar.MONDAY) minimalDaysInFirstWeek = 7 set(Calendar.YEAR, year) var weeksData = mutableListOf<MutableList<Long>>() for (i in 1..getMaxWeekOfYear(year)) { var daysData = getDaysOfWeek(year, i) weeksData.add(daysData) } return weeksData } /** * 获取指定时间段内的所有周(周包含指定时间) * @param startDate 开始时间 * @param endDate 结束时间 * @param startContain 是否包含开始时间所在周 * @param endContain 是否包含结束时间所在周 */ internal fun Calendar.getWeeks( startDate: Long = 0L, endDate: Long = 0L, startContain: Boolean = true, endContain: Boolean = true ): MutableList<MutableList<Long>> { if ((startDate != 0L && endDate != 0L) && (startDate > endDate)) throw Exception("startDate > endDate") val startYear by lazy { if (startDate <= 0) Calendar.getInstance().get(Calendar.YEAR) else { timeInMillis = startDate get(Calendar.YEAR) } } val endYear by lazy { if (endDate <= 0) Calendar.getInstance().get(Calendar.YEAR) else { timeInMillis = endDate get(Calendar.YEAR) } } //获取时间段内所有年的周数据 var weeksData = mutableListOf<MutableList<Long>>() for (year in startYear..endYear) { weeksData.addAll(getWeeksOfYear(year)) } //移除不在时间段内的周数据 val weekIterator = weeksData.iterator() while (weekIterator.hasNext()) { val week = weekIterator.next() if ((startDate > 0 && week[week.size - 1] < startDate) || (endDate > 0 && week[0] > endDate)) weekIterator.remove() if (!startContain && week.contain(startDate)) weekIterator.remove() if (!endContain && week.contain(endDate)) weekIterator.remove() } return weeksData } /** * 获取一年中的最后一周数字 * @param year 1900-9999 * @return week 52 or 53 */ internal fun Calendar.getMaxWeekOfYear(year: Int = Calendar.getInstance().get(Calendar.YEAR)): Int { set(year, Calendar.DECEMBER, 31, 0, 0, 0) return getWeekOfYear(time) } /** * 获取 date 所在年的周数 * @param date 时间 * @return int */ internal fun Calendar.getWeekOfYear(date: Date): Int { firstDayOfWeek = Calendar.MONDAY minimalDaysInFirstWeek = 7 time = date return get(Calendar.WEEK_OF_YEAR) } /** * 获取某年某周的日期时间戳集合[第一天-最后一天] * @param year 1900-9999 * @param week 1-52/53 * @return MutableList<Long> */ internal fun Calendar.getDaysOfWeek( year: Int = Calendar.getInstance().get(Calendar.YEAR), week: Int ): MutableList<Long> { if (year < 1900 || year > 9999) throw NullPointerException("The year must be within 1900-9999") firstDayOfWeek = Calendar.MONDAY set(Calendar.DAY_OF_WEEK, Calendar.MONDAY) minimalDaysInFirstWeek = 7 set(Calendar.YEAR, year) set(Calendar.WEEK_OF_YEAR, week) var weekData = mutableListOf<Long>() for (i in 0 until 7) { weekData.add(timeInMillis + (86400000 * i)) } return weekData } /** * 获取一年最多有多少天 * * @param year * @return */ internal fun GregorianCalendar.getMaxDayAtYear(year: Int): Int { set(Calendar.YEAR, year) return (if (isLeapYear(year)) 1 else 0) + 365 } /** * 获取一月中最大的一天 */ internal fun Calendar.getMaxDayInMonth(): Int { return this.getActualMaximum(Calendar.DAY_OF_MONTH) } /** * 与传入日历是否为同一年 * @param calendar */ internal fun Calendar.isSameYear(calendar: Calendar): Boolean { return get(Calendar.YEAR) == calendar.get(Calendar.YEAR) } /** * 与传入日历是否为同一月 * @param calendar */ internal fun Calendar.isSameMonth(calendar: Calendar): Boolean { return isSameYear(calendar) && get(Calendar.MONTH) == calendar.get(Calendar.MONTH) } /** * 与传入日历是否为同一天 * @param calendar */ internal fun Calendar.isSameDay(calendar: Calendar): Boolean { return isSameYear(calendar) && get(Calendar.DAY_OF_YEAR) == calendar.get(Calendar.DAY_OF_YEAR) } /** * 与传入日历是否为同一时 * @param calendar */ internal fun Calendar.isSameHour(calendar: Calendar): Boolean { return isSameDay(calendar) && get(Calendar.HOUR_OF_DAY) == calendar.get(Calendar.HOUR_OF_DAY) } /** * 与传入日历是否为同一分 * @param calendar */ internal fun Calendar.isSameMinute(calendar: Calendar): Boolean { return isSameHour(calendar) && get(Calendar.MINUTE) == calendar.get(Calendar.MINUTE) } /** * 与传入日历是否为同一秒 * @param calendar */ internal fun Calendar.isSameSecond(calendar: Calendar): Boolean { return isSameMinute(calendar) && get(Calendar.SECOND) == calendar.get(Calendar.SECOND) } date_time_picker/src/main/java/com/loper7/date_time_picker/ext/ContextExt.kt
New file @@ -0,0 +1,26 @@ package com.loper7.tab_expand.ext import android.content.Context /** * * @CreateDate: 2020/8/18 11:15 * @Description: java类作用描述 * @Author: LOPER7 * @Email: loper7@163.com */ /** * 根据手机的分辨率dp 转成px(像素) */ internal fun Context.dip2px(dpValue: Float): Int { val scale = resources.displayMetrics.density return (dpValue * scale + 0.5f).toInt() } /** * 根据手机的分辨率px(像素) 转成dp */ internal fun Context.px2dip(pxValue: Float): Int { val scale = resources.displayMetrics.density return (pxValue / scale + 0.5f).toInt() } date_time_picker/src/main/java/com/loper7/date_time_picker/ext/ListExt.kt
New file @@ -0,0 +1,49 @@ package com.loper7.date_time_picker.ext import android.util.Log import com.loper7.date_time_picker.utils.StringUtils import java.util.* /** * 将时间戳集合格式化为指定日期格式的集合 * @return MutableList<String> [2021-09-09,2021--09-10,...] */ internal fun MutableList<Long>.toFormatList(format: String = "yyyy-MM-dd"): MutableList<String> { var formatList = mutableListOf<String>() for (i in this) { formatList.add(StringUtils.conversionTime(i, format)) } return formatList } /** * 时间集合内是否包含对应某天 */ internal fun MutableList<Long>.contain(date: Long): Boolean { for (i in this) { if (StringUtils.conversionTime(i, "yyyyMMdd") == StringUtils.conversionTime( date, "yyyyMMdd" ) ) { return true } } return false } /** * 获取对应时间所在周的下标 */ internal fun MutableList<MutableList<Long>>.index(date: Long?): Int { if (this.isNullOrEmpty() || date == null) return -1 var _date = date if (_date == 0L) _date = Calendar.getInstance().timeInMillis for (i in 0 until size) { if (this[i].contain(_date)) return i } return 0 } date_time_picker/src/main/java/com/loper7/date_time_picker/number_picker/NumberPicker.java
New file @@ -0,0 +1,3051 @@ package com.loper7.date_time_picker.number_picker; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.InputType; import android.text.Spanned; import android.text.TextUtils; import android.text.method.NumberKeyListener; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.SparseArray; import android.util.TypedValue; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.LayoutInflater.Filter; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.widget.EditText; import android.widget.LinearLayout; import androidx.annotation.CallSuper; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.DimenRes; import androidx.annotation.IntDef; import androidx.annotation.StringRes; import androidx.core.content.ContextCompat; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.Locale; import static java.lang.annotation.RetentionPolicy.SOURCE; import com.loper7.date_time_picker.R; /** * A widget that enables the user to select a number from a predefined range. */ public class NumberPicker extends LinearLayout { @Retention(SOURCE) @IntDef({VERTICAL, HORIZONTAL}) public @interface Orientation { } public static final int VERTICAL = LinearLayout.VERTICAL; public static final int HORIZONTAL = LinearLayout.HORIZONTAL; @Retention(SOURCE) @IntDef({ASCENDING, DESCENDING}) public @interface Order { } public static final int ASCENDING = 0; public static final int DESCENDING = 1; @Retention(SOURCE) @IntDef({LEFT, CENTER, RIGHT}) public @interface Align { } public static final int RIGHT = 0; public static final int CENTER = 1; public static final int LEFT = 2; @Retention(SOURCE) @IntDef({SIDE_LINES, UNDERLINE}) public @interface DividerType { } public static final int SIDE_LINES = 0; public static final int UNDERLINE = 1; /** * The default update interval during long press. */ private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300; /** * The default coefficient to adjust (divide) the max fling velocity. */ private static final int DEFAULT_MAX_FLING_VELOCITY_COEFFICIENT = 8; /** * The the duration for adjusting the selector wheel. */ private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; /** * The duration of scrolling while snapping to a given position. */ private static final int SNAP_SCROLL_DURATION = 300; /** * The default strength of fading edge while drawing the selector. */ private static final float DEFAULT_FADING_EDGE_STRENGTH = 0.9f; /** * The default unscaled height of the divider. */ private static final int UNSCALED_DEFAULT_DIVIDER_THICKNESS = 2; /** * The default unscaled distance between the dividers. */ private static final int UNSCALED_DEFAULT_DIVIDER_DISTANCE = 48; /** * Constant for unspecified size. */ private static final int SIZE_UNSPECIFIED = -1; /** * The default color of divider. */ private static final int DEFAULT_DIVIDER_COLOR = 0xFF000000; /** * The default max value of this widget. */ private static final int DEFAULT_MAX_VALUE = 100; /** * The default min value of this widget. */ private static final int DEFAULT_MIN_VALUE = 1; /** * The default wheel item count of this widget. */ private static final int DEFAULT_WHEEL_ITEM_COUNT = 3; /** * The default max height of this widget. */ private static final int DEFAULT_MAX_HEIGHT = 180; /** * The default min width of this widget. */ private static final int DEFAULT_MIN_WIDTH = 58; /** * The default align of text. */ private static final int DEFAULT_TEXT_ALIGN = CENTER; /** * The default color of text. */ private static final int DEFAULT_TEXT_COLOR = 0xFF000000; /** * The default size of text. */ private static final float DEFAULT_TEXT_SIZE = 15f; /** * The default line spacing multiplier of text. */ private static final float DEFAULT_LINE_SPACING_MULTIPLIER = 1f; /** * The description of the current value. */ private String label = ""; private boolean textBold = true; private boolean selectedTextBold = true; /** * Use a custom NumberPicker formatting callback to use two-digit minutes * strings like "01". Keeping a static formatter etc. is the most efficient * way to do this; it avoids creating temporary objects on every call to * format(). */ private static class TwoDigitFormatter implements Formatter { final StringBuilder mBuilder = new StringBuilder(); char mZeroDigit; java.util.Formatter mFmt; final Object[] mArgs = new Object[1]; TwoDigitFormatter() { final Locale locale = Locale.getDefault(); init(locale); } private void init(Locale locale) { mFmt = createFormatter(locale); mZeroDigit = getZeroDigit(locale); } public String format(int value) { final Locale currentLocale = Locale.getDefault(); if (mZeroDigit != getZeroDigit(currentLocale)) { init(currentLocale); } mArgs[0] = value; mBuilder.delete(0, mBuilder.length()); mFmt.format("%02d", mArgs); return mFmt.toString(); } private static char getZeroDigit(Locale locale) { // return LocaleData.get(locale).zeroDigit; return new DecimalFormatSymbols(locale).getZeroDigit(); } private java.util.Formatter createFormatter(Locale locale) { return new java.util.Formatter(mBuilder, locale); } } private static final TwoDigitFormatter sTwoDigitFormatter = new TwoDigitFormatter(); public static Formatter getTwoDigitFormatter() { return sTwoDigitFormatter; } /** * The text for showing the current value. */ private final EditText mSelectedText; /** * The center X position of the selected text. */ private float mSelectedTextCenterX; /** * The center Y position of the selected text. */ private float mSelectedTextCenterY; /** * The min height of this widget. */ private int mMinHeight; /** * The max height of this widget. */ private int mMaxHeight; /** * The max width of this widget. */ private int mMinWidth; /** * The max width of this widget. */ private int mMaxWidth; /** * Flag whether to compute the max width. */ private final boolean mComputeMaxWidth; /** * The align of the selected text. */ private int mSelectedTextAlign = DEFAULT_TEXT_ALIGN; /** * The color of the selected text. */ private int mSelectedTextColor = DEFAULT_TEXT_COLOR; /** * The size of the selected text. */ private float mSelectedTextSize = DEFAULT_TEXT_SIZE; /** * Flag whether the selected text should strikethroughed. */ private boolean mSelectedTextStrikeThru; /** * Flag whether the selected text should underlined. */ private boolean mSelectedTextUnderline; /** * The typeface of the selected text. */ private Typeface mSelectedTypeface; /** * The align of the text. */ private int mTextAlign = DEFAULT_TEXT_ALIGN; /** * The color of the text. */ private int mTextColor = DEFAULT_TEXT_COLOR; /** * The size of the text. */ private float mTextSize = DEFAULT_TEXT_SIZE; /** * Flag whether the text should strikethroughed. */ private boolean mTextStrikeThru; /** * Flag whether the text should underlined. */ private boolean mTextUnderline; /** * The typeface of the text. */ private Typeface mTypeface; /** * The width of the gap between text elements if the selector wheel. */ private int mSelectorTextGapWidth; /** * The height of the gap between text elements if the selector wheel. */ private int mSelectorTextGapHeight; /** * The values to be displayed instead the indices. */ private String[] mDisplayedValues; /** * Lower value of the range of numbers allowed for the NumberPicker */ private int mMinValue = DEFAULT_MIN_VALUE; /** * Upper value of the range of numbers allowed for the NumberPicker */ private int mMaxValue = DEFAULT_MAX_VALUE; /** * Current value of this NumberPicker */ private int mValue; /** * Listener to be notified upon current value click. */ private OnClickListener mOnClickListener; /** * Listener to be notified upon current value change. */ private OnValueChangeListener mOnValueChangeListener; /** * Listener to be notified upon scroll state change. */ private OnScrollListener mOnScrollListener; /** * Formatter for for displaying the current value. */ private Formatter mFormatter; /** * The speed for updating the value form long press. */ private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL; /** * Cache for the string representation of selector indices. */ private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<>(); /** * The number of items show in the selector wheel. */ private int mWheelItemCount = DEFAULT_WHEEL_ITEM_COUNT; /** * The real number of items show in the selector wheel. */ private int mRealWheelItemCount = DEFAULT_WHEEL_ITEM_COUNT; /** * The index of the middle selector item. */ private int mWheelMiddleItemIndex = mWheelItemCount / 2; /** * The selector indices whose value are show by the selector. */ private int[] mSelectorIndices = new int[mWheelItemCount]; /** * The {@link Paint} for drawing the selector. */ private final Paint mSelectorWheelPaint; /** * The size of a selector element (text + gap). */ private int mSelectorElementSize; /** * The initial offset of the scroll selector. */ private int mInitialScrollOffset = Integer.MIN_VALUE; /** * The current offset of the scroll selector. */ private int mCurrentScrollOffset; /** * The {@link Scroller} responsible for flinging the selector. */ private final Scroller mFlingScroller; /** * The {@link Scroller} responsible for adjusting the selector. */ private final Scroller mAdjustScroller; /** * The previous X coordinate while scrolling the selector. */ private int mPreviousScrollerX; /** * The previous Y coordinate while scrolling the selector. */ private int mPreviousScrollerY; /** * Handle to the reusable command for setting the input text selection. */ private SetSelectionCommand mSetSelectionCommand; /** * Handle to the reusable command for changing the current value from long press by one. */ private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand; /** * The X position of the last down event. */ private float mLastDownEventX; /** * The Y position of the last down event. */ private float mLastDownEventY; /** * The X position of the last down or move event. */ private float mLastDownOrMoveEventX; /** * The Y position of the last down or move event. */ private float mLastDownOrMoveEventY; /** * Determines speed during touch scrolling. */ private VelocityTracker mVelocityTracker; /** * @see ViewConfiguration#getScaledTouchSlop() */ private int mTouchSlop; /** * @see ViewConfiguration#getScaledMinimumFlingVelocity() */ private int mMinimumFlingVelocity; /** * @see ViewConfiguration#getScaledMaximumFlingVelocity() */ private int mMaximumFlingVelocity; /** * Flag whether the selector should wrap around. */ private boolean mWrapSelectorWheel; /** * User choice on whether the selector wheel should be wrapped. */ private boolean mWrapSelectorWheelPreferred = true; /** * Divider for showing item to be selected while scrolling */ private Drawable mDividerDrawable; /** * The color of the divider. */ private int mDividerColor = DEFAULT_DIVIDER_COLOR; /** * The distance between the two dividers. */ private int mDividerDistance; /** * The thickness of the divider. */ private int mDividerLength; /** * The thickness of the divider. */ private int mDividerThickness; /** * The top of the top divider. */ private int mTopDividerTop; /** * The bottom of the bottom divider. */ private int mBottomDividerBottom; /** * The left of the top divider. */ private int mLeftDividerLeft; /** * The right of the right divider. */ private int mRightDividerRight; /** * The type of the divider. */ private int mDividerType; /** * The current scroll state of the number picker. */ private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; /** * The keycode of the last handled DPAD down event. */ private int mLastHandledDownDpadKeyCode = -1; /** * Flag whether the selector wheel should hidden until the picker has focus. */ private boolean mHideWheelUntilFocused; /** * The orientation of this widget. */ private int mOrientation; /** * The order of this widget. */ private int mOrder; /** * Flag whether the fading edge should enabled. */ private boolean mFadingEdgeEnabled = true; /** * The strength of fading edge while drawing the selector. */ private float mFadingEdgeStrength = DEFAULT_FADING_EDGE_STRENGTH; /** * Flag whether the scroller should enabled. */ private boolean mScrollerEnabled = true; /** * The line spacing multiplier of the text. */ private float mLineSpacingMultiplier = DEFAULT_LINE_SPACING_MULTIPLIER; /** * The coefficient to adjust (divide) the max fling velocity. */ private int mMaxFlingVelocityCoefficient = DEFAULT_MAX_FLING_VELOCITY_COEFFICIENT; /** * Flag whether the accessibility description enabled. */ private boolean mAccessibilityDescriptionEnabled = true; /** * The context of this widget. */ private Context mContext; /** * The number formatter for current locale. */ private NumberFormat mNumberFormatter; /** * The view configuration of this widget. */ private ViewConfiguration mViewConfiguration; /** * Interface to listen for changes of the current value. */ public interface OnValueChangeListener { /** * Called upon a change of the current value. * * @param picker The NumberPicker associated with this listener. * @param oldVal The previous value. * @param newVal The new value. */ void onValueChange(NumberPicker picker, int oldVal, int newVal); } /** * The amount of space between items. */ private int mItemSpacing = 0; /** * Interface to listen for the picker scroll state. */ public interface OnScrollListener { @IntDef({SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING}) @Retention(RetentionPolicy.SOURCE) public @interface ScrollState { } /** * The view is not scrolling. */ public static int SCROLL_STATE_IDLE = 0; /** * The user is scrolling using touch, and his finger is still on the screen. */ public static int SCROLL_STATE_TOUCH_SCROLL = 1; /** * The user had previously been scrolling using touch and performed a fling. */ public static int SCROLL_STATE_FLING = 2; /** * Callback invoked while the number picker scroll state has changed. * * @param view The view whose scroll state is being reported. * @param scrollState The current scroll state. One of * {@link #SCROLL_STATE_IDLE}, * {@link #SCROLL_STATE_TOUCH_SCROLL} or * {@link #SCROLL_STATE_IDLE}. */ public void onScrollStateChange(NumberPicker view, @ScrollState int scrollState); } /** * Interface used to format current value into a string for presentation. */ public interface Formatter { /** * Formats a string representation of the current value. * * @param value The currently selected value. * @return A formatted string representation. */ public String format(int value); } /** * Create a new number picker. * * @param context The application environment. */ public NumberPicker(Context context) { this(context, null); } /** * Create a new number picker. * * @param context The application environment. * @param attrs A collection of attributes. */ public NumberPicker(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * Create a new number picker * * @param context the application environment. * @param attrs a collection of attributes. * @param defStyle The default style to apply to this view. */ public NumberPicker(Context context, AttributeSet attrs, int defStyle) { super(context, attrs); mContext = context; mNumberFormatter = NumberFormat.getInstance(); final TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.NumberPicker, defStyle, 0); final Drawable selectionDivider = attributes.getDrawable( R.styleable.NumberPicker_np_divider); if (selectionDivider != null) { selectionDivider.setCallback(this); if (selectionDivider.isStateful()) { selectionDivider.setState(getDrawableState()); } mDividerDrawable = selectionDivider; } else { mDividerColor = attributes.getColor(R.styleable.NumberPicker_np_dividerColor, mDividerColor); setDividerColor(mDividerColor); } final DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); final int defDividerDistance = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_DIVIDER_DISTANCE, displayMetrics); final int defDividerThickness = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_DIVIDER_THICKNESS, displayMetrics); mDividerDistance = attributes.getDimensionPixelSize( R.styleable.NumberPicker_np_dividerDistance, defDividerDistance); mDividerLength = attributes.getDimensionPixelSize( R.styleable.NumberPicker_np_dividerLength, 0); mDividerThickness = attributes.getDimensionPixelSize( R.styleable.NumberPicker_np_dividerThickness, defDividerThickness); mDividerType = attributes.getInt(R.styleable.NumberPicker_np_dividerType, SIDE_LINES); mOrder = attributes.getInt(R.styleable.NumberPicker_np_order, ASCENDING); mOrientation = attributes.getInt(R.styleable.NumberPicker_np_orientation, VERTICAL); final float width = attributes.getDimensionPixelSize(R.styleable.NumberPicker_np_width, SIZE_UNSPECIFIED); final float height = attributes.getDimensionPixelSize(R.styleable.NumberPicker_np_height, SIZE_UNSPECIFIED); setWidthAndHeight(); mComputeMaxWidth = true; mValue = attributes.getInt(R.styleable.NumberPicker_np_value, mValue); mMaxValue = attributes.getInt(R.styleable.NumberPicker_np_max, mMaxValue); mMinValue = attributes.getInt(R.styleable.NumberPicker_np_min, mMinValue); mSelectedTextAlign = attributes.getInt(R.styleable.NumberPicker_np_selectedTextAlign, mSelectedTextAlign); mSelectedTextColor = attributes.getColor(R.styleable.NumberPicker_np_selectedTextColor, mSelectedTextColor); mSelectedTextSize = attributes.getDimension(R.styleable.NumberPicker_np_selectedTextSize, spToPx(mSelectedTextSize)); mSelectedTextStrikeThru = attributes.getBoolean( R.styleable.NumberPicker_np_selectedTextStrikeThru, mSelectedTextStrikeThru); mSelectedTextUnderline = attributes.getBoolean( R.styleable.NumberPicker_np_selectedTextUnderline, mSelectedTextUnderline); mSelectedTypeface = Typeface.create(attributes.getString( R.styleable.NumberPicker_np_selectedTypeface), Typeface.NORMAL); mTextAlign = attributes.getInt(R.styleable.NumberPicker_np_textAlign, mTextAlign); mTextColor = attributes.getColor(R.styleable.NumberPicker_np_textColor, mTextColor); mTextSize = attributes.getDimension(R.styleable.NumberPicker_np_textSize, spToPx(mTextSize)); mTextStrikeThru = attributes.getBoolean( R.styleable.NumberPicker_np_textStrikeThru, mTextStrikeThru); mTextUnderline = attributes.getBoolean( R.styleable.NumberPicker_np_textUnderline, mTextUnderline); mTypeface = Typeface.create(attributes.getString(R.styleable.NumberPicker_np_typeface), Typeface.NORMAL); mFormatter = stringToFormatter(attributes.getString(R.styleable.NumberPicker_np_formatter)); mFadingEdgeEnabled = attributes.getBoolean(R.styleable.NumberPicker_np_fadingEdgeEnabled, mFadingEdgeEnabled); mFadingEdgeStrength = attributes.getFloat(R.styleable.NumberPicker_np_fadingEdgeStrength, mFadingEdgeStrength); mScrollerEnabled = attributes.getBoolean(R.styleable.NumberPicker_np_scrollerEnabled, mScrollerEnabled); mWheelItemCount = attributes.getInt(R.styleable.NumberPicker_np_wheelItemCount, mWheelItemCount); mLineSpacingMultiplier = attributes.getFloat( R.styleable.NumberPicker_np_lineSpacingMultiplier, mLineSpacingMultiplier); mMaxFlingVelocityCoefficient = attributes.getInt( R.styleable.NumberPicker_np_maxFlingVelocityCoefficient, mMaxFlingVelocityCoefficient); mHideWheelUntilFocused = attributes.getBoolean( R.styleable.NumberPicker_np_hideWheelUntilFocused, false); mAccessibilityDescriptionEnabled = attributes.getBoolean( R.styleable.NumberPicker_np_accessibilityDescriptionEnabled, true); mItemSpacing = attributes.getDimensionPixelSize( R.styleable.NumberPicker_np_itemSpacing, 0); textBold = attributes.getBoolean( R.styleable.NumberPicker_np_textBold, textBold); selectedTextBold = attributes.getBoolean( R.styleable.NumberPicker_np_selectedTextBold, selectedTextBold); // By default LinearLayout that we extend is not drawn. This is // its draw() method is not called but dispatchDraw() is called // directly (see ViewGroup.drawChild()). However, this class uses // the fading edge effect implemented by View and we need our // draw() method to be called. Therefore, we declare we will draw. setWillNotDraw(false); // input text mSelectedText = new EditText(context); mSelectedText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT)); mSelectedText.setGravity(Gravity.CENTER); mSelectedText.setSingleLine(true); mSelectedText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); mSelectedText.setEnabled(false); mSelectedText.setFocusable(false); mSelectedText.setVisibility(View.INVISIBLE); mSelectedText.setImeOptions(EditorInfo.IME_ACTION_NONE); // create the selector wheel paint Paint paint = new Paint(); paint.setAntiAlias(true); paint.setTextAlign(Paint.Align.CENTER); mSelectorWheelPaint = paint; setSelectedTextColor(mSelectedTextColor); setTextColor(mTextColor); setTextSize(mTextSize); setSelectedTextSize(mSelectedTextSize); setTypeface(mTypeface); setSelectedTypeface(mSelectedTypeface); setFormatter(mFormatter); updateInputTextView(); setValue(mValue); setMaxValue(mMaxValue); setMinValue(mMinValue); setWheelItemCount(mWheelItemCount); mWrapSelectorWheel = attributes.getBoolean(R.styleable.NumberPicker_np_wrapSelectorWheel, mWrapSelectorWheel); setWrapSelectorWheel(mWrapSelectorWheel); if (width != SIZE_UNSPECIFIED && height != SIZE_UNSPECIFIED) { setScaleX(width / mMinWidth); setScaleY(height / mMaxHeight); } else if (width != SIZE_UNSPECIFIED) { final float scale = width / mMinWidth; setScaleX(scale); setScaleY(scale); } else if (height != SIZE_UNSPECIFIED) { final float scale = height / mMaxHeight; setScaleX(scale); setScaleY(scale); } // initialize constants mViewConfiguration = ViewConfiguration.get(context); mTouchSlop = mViewConfiguration.getScaledTouchSlop(); mMinimumFlingVelocity = mViewConfiguration.getScaledMinimumFlingVelocity(); mMaximumFlingVelocity = mViewConfiguration.getScaledMaximumFlingVelocity() / mMaxFlingVelocityCoefficient; // create the fling and adjust scrollers mFlingScroller = new Scroller(context, null, true); mAdjustScroller = new Scroller(context, new DecelerateInterpolator(2.5f)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { // If not explicitly specified this view is important for accessibility. if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Should be focusable by default, as the text view whose visibility changes is focusable if (getFocusable() == View.FOCUSABLE_AUTO) { setFocusable(View.FOCUSABLE); setFocusableInTouchMode(true); } } attributes.recycle(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int msrdWdth = getMeasuredWidth(); final int msrdHght = getMeasuredHeight(); // Input text centered horizontally. final int inptTxtMsrdWdth = mSelectedText.getMeasuredWidth(); final int inptTxtMsrdHght = mSelectedText.getMeasuredHeight(); final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2; final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2; final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth; final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght; mSelectedText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom); mSelectedTextCenterX = mSelectedText.getX() + mSelectedText.getMeasuredWidth() / 2f - 2f; mSelectedTextCenterY = mSelectedText.getY() + mSelectedText.getMeasuredHeight() / 2f - 5f; if (changed) { // need to do all this when we know our size initializeSelectorWheel(); initializeFadingEdges(); final int dividerDistance = 2 * mDividerThickness + mDividerDistance; if (isHorizontalMode()) { mLeftDividerLeft = (getWidth() - mDividerDistance) / 2 - mDividerThickness; mRightDividerRight = mLeftDividerLeft + dividerDistance; mBottomDividerBottom = getHeight(); } else { mTopDividerTop = (getHeight() - mDividerDistance) / 2 - mDividerThickness; mBottomDividerBottom = mTopDividerTop + dividerDistance; } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Try greedily to fit the max width and height. final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth); final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight); super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec); // Flag if we are measured with width or height less than the respective min. final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(), widthMeasureSpec); final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(), heightMeasureSpec); setMeasuredDimension(widthSize, heightSize); } /** * Move to the final position of a scroller. Ensures to force finish the scroller * and if it is not at its final position a scroll of the selector wheel is * performed to fast forward to the final position. * * @param scroller The scroller to whose final position to get. * @return True of the a move was performed, i.e. the scroller was not in final position. */ private boolean moveToFinalScrollerPosition(Scroller scroller) { scroller.forceFinished(true); if (isHorizontalMode()) { int amountToScroll = scroller.getFinalX() - scroller.getCurrX(); int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementSize; int overshootAdjustment = mInitialScrollOffset - futureScrollOffset; if (overshootAdjustment != 0) { if (Math.abs(overshootAdjustment) > mSelectorElementSize / 2) { if (overshootAdjustment > 0) { overshootAdjustment -= mSelectorElementSize; } else { overshootAdjustment += mSelectorElementSize; } } amountToScroll += overshootAdjustment; scrollBy(amountToScroll, 0); return true; } } else { int amountToScroll = scroller.getFinalY() - scroller.getCurrY(); int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementSize; int overshootAdjustment = mInitialScrollOffset - futureScrollOffset; if (overshootAdjustment != 0) { if (Math.abs(overshootAdjustment) > mSelectorElementSize / 2) { if (overshootAdjustment > 0) { overshootAdjustment -= mSelectorElementSize; } else { overshootAdjustment += mSelectorElementSize; } } amountToScroll += overshootAdjustment; scrollBy(0, amountToScroll); return true; } } return false; } @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (!isEnabled()) { return false; } final int action = event.getAction() & MotionEvent.ACTION_MASK; if (action != MotionEvent.ACTION_DOWN) { return false; } removeAllCallbacks(); // Make sure we support flinging inside scrollables. getParent().requestDisallowInterceptTouchEvent(true); if (isHorizontalMode()) { mLastDownOrMoveEventX = mLastDownEventX = event.getX(); if (!mFlingScroller.isFinished()) { mFlingScroller.forceFinished(true); mAdjustScroller.forceFinished(true); onScrollerFinished(mFlingScroller); onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } else if (!mAdjustScroller.isFinished()) { mFlingScroller.forceFinished(true); mAdjustScroller.forceFinished(true); onScrollerFinished(mAdjustScroller); } else if (mLastDownEventX >= mLeftDividerLeft && mLastDownEventX <= mRightDividerRight) { if (mOnClickListener != null) { mOnClickListener.onClick(this); } } else if (mLastDownEventX < mLeftDividerLeft) { postChangeCurrentByOneFromLongPress(false); } else if (mLastDownEventX > mRightDividerRight) { postChangeCurrentByOneFromLongPress(true); } } else { mLastDownOrMoveEventY = mLastDownEventY = event.getY(); if (!mFlingScroller.isFinished()) { mFlingScroller.forceFinished(true); mAdjustScroller.forceFinished(true); onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } else if (!mAdjustScroller.isFinished()) { mFlingScroller.forceFinished(true); mAdjustScroller.forceFinished(true); } else if (mLastDownEventY >= mTopDividerTop && mLastDownEventY <= mBottomDividerBottom) { if (mOnClickListener != null) { mOnClickListener.onClick(this); } } else if (mLastDownEventY < mTopDividerTop) { postChangeCurrentByOneFromLongPress(false); } else if (mLastDownEventY > mBottomDividerBottom) { postChangeCurrentByOneFromLongPress(true); } } return true; } @Override public boolean onTouchEvent(MotionEvent event) { if (!isEnabled()) { return false; } if (!isScrollerEnabled()) { return false; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); int action = event.getAction() & MotionEvent.ACTION_MASK; switch (action) { case MotionEvent.ACTION_MOVE: { if (isHorizontalMode()) { float currentMoveX = event.getX(); if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { int deltaDownX = (int) Math.abs(currentMoveX - mLastDownEventX); if (deltaDownX > mTouchSlop) { removeAllCallbacks(); onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } } else { int deltaMoveX = (int) ((currentMoveX - mLastDownOrMoveEventX)); scrollBy(deltaMoveX, 0); invalidate(); } mLastDownOrMoveEventX = currentMoveX; } else { float currentMoveY = event.getY(); if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); if (deltaDownY > mTouchSlop) { removeAllCallbacks(); onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } } else { int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY)); scrollBy(0, deltaMoveY); invalidate(); } mLastDownOrMoveEventY = currentMoveY; } } break; case MotionEvent.ACTION_UP: { removeChangeCurrentByOneFromLongPress(); VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); if (isHorizontalMode()) { int initialVelocity = (int) velocityTracker.getXVelocity(); if (Math.abs(initialVelocity) > mMinimumFlingVelocity) { fling(initialVelocity); onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); } else { int eventX = (int) event.getX(); int deltaMoveX = (int) Math.abs(eventX - mLastDownEventX); if (deltaMoveX <= mTouchSlop) { int selectorIndexOffset = (eventX / mSelectorElementSize) - mWheelMiddleItemIndex; if (selectorIndexOffset > 0) { changeValueByOne(true); } else if (selectorIndexOffset < 0) { changeValueByOne(false); } else { ensureScrollWheelAdjusted(); } } else { ensureScrollWheelAdjusted(); } onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } } else { int initialVelocity = (int) velocityTracker.getYVelocity(); if (Math.abs(initialVelocity) > mMinimumFlingVelocity) { fling(initialVelocity); onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); } else { int eventY = (int) event.getY(); int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY); if (deltaMoveY <= mTouchSlop) { int selectorIndexOffset = (eventY / mSelectorElementSize) - mWheelMiddleItemIndex; if (selectorIndexOffset > 0) { changeValueByOne(true); } else if (selectorIndexOffset < 0) { changeValueByOne(false); } else { ensureScrollWheelAdjusted(); } } else { ensureScrollWheelAdjusted(); } onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } } mVelocityTracker.recycle(); mVelocityTracker = null; } break; } return true; } @Override public boolean dispatchTouchEvent(MotionEvent event) { final int action = event.getAction() & MotionEvent.ACTION_MASK; switch (action) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: removeAllCallbacks(); break; } return super.dispatchTouchEvent(event); } @Override public boolean dispatchKeyEvent(KeyEvent event) { final int keyCode = event.getKeyCode(); switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: removeAllCallbacks(); break; case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_UP: switch (event.getAction()) { case KeyEvent.ACTION_DOWN: if (mWrapSelectorWheel || ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN) ? getValue() < getMaxValue() : getValue() > getMinValue())) { requestFocus(); mLastHandledDownDpadKeyCode = keyCode; removeAllCallbacks(); if (mFlingScroller.isFinished()) { changeValueByOne(keyCode == KeyEvent.KEYCODE_DPAD_DOWN); } return true; } break; case KeyEvent.ACTION_UP: if (mLastHandledDownDpadKeyCode == keyCode) { mLastHandledDownDpadKeyCode = -1; return true; } break; } } return super.dispatchKeyEvent(event); } @Override public boolean dispatchTrackballEvent(MotionEvent event) { final int action = event.getAction() & MotionEvent.ACTION_MASK; switch (action) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: removeAllCallbacks(); break; } return super.dispatchTrackballEvent(event); } @Override public void computeScroll() { if (!isScrollerEnabled()) { return; } Scroller scroller = mFlingScroller; if (scroller.isFinished()) { scroller = mAdjustScroller; if (scroller.isFinished()) { return; } } scroller.computeScrollOffset(); if (isHorizontalMode()) { int currentScrollerX = scroller.getCurrX(); if (mPreviousScrollerX == 0) { mPreviousScrollerX = scroller.getStartX(); } scrollBy(currentScrollerX - mPreviousScrollerX, 0); mPreviousScrollerX = currentScrollerX; } else { int currentScrollerY = scroller.getCurrY(); if (mPreviousScrollerY == 0) { mPreviousScrollerY = scroller.getStartY(); } scrollBy(0, currentScrollerY - mPreviousScrollerY); mPreviousScrollerY = currentScrollerY; } if (scroller.isFinished()) { onScrollerFinished(scroller); } else { postInvalidate(); } } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); mSelectedText.setEnabled(enabled); } @Override public void scrollBy(int x, int y) { if (!isScrollerEnabled()) { return; } int[] selectorIndices = getSelectorIndices(); int startScrollOffset = mCurrentScrollOffset; int gap = (int) getMaxTextSize(); if (isHorizontalMode()) { if (isAscendingOrder()) { if (!mWrapSelectorWheel && x > 0 && selectorIndices[mWheelMiddleItemIndex] <= mMinValue) { mCurrentScrollOffset = mInitialScrollOffset; return; } if (!mWrapSelectorWheel && x < 0 && selectorIndices[mWheelMiddleItemIndex] >= mMaxValue) { mCurrentScrollOffset = mInitialScrollOffset; return; } } else { if (!mWrapSelectorWheel && x > 0 && selectorIndices[mWheelMiddleItemIndex] >= mMaxValue) { mCurrentScrollOffset = mInitialScrollOffset; return; } if (!mWrapSelectorWheel && x < 0 && selectorIndices[mWheelMiddleItemIndex] <= mMinValue) { mCurrentScrollOffset = mInitialScrollOffset; return; } } mCurrentScrollOffset += x; } else { if (isAscendingOrder()) { if (!mWrapSelectorWheel && y > 0 && selectorIndices[mWheelMiddleItemIndex] <= mMinValue) { mCurrentScrollOffset = mInitialScrollOffset; return; } if (!mWrapSelectorWheel && y < 0 && selectorIndices[mWheelMiddleItemIndex] >= mMaxValue) { mCurrentScrollOffset = mInitialScrollOffset; return; } } else { if (!mWrapSelectorWheel && y > 0 && selectorIndices[mWheelMiddleItemIndex] >= mMaxValue) { mCurrentScrollOffset = mInitialScrollOffset; return; } if (!mWrapSelectorWheel && y < 0 && selectorIndices[mWheelMiddleItemIndex] <= mMinValue) { mCurrentScrollOffset = mInitialScrollOffset; return; } } mCurrentScrollOffset += y; } while (mCurrentScrollOffset - mInitialScrollOffset > gap) { mCurrentScrollOffset -= mSelectorElementSize; if (isAscendingOrder()) { decrementSelectorIndices(selectorIndices); } else { incrementSelectorIndices(selectorIndices); } setValueInternal(selectorIndices[mWheelMiddleItemIndex], true); if (!mWrapSelectorWheel && selectorIndices[mWheelMiddleItemIndex] < mMinValue) { mCurrentScrollOffset = mInitialScrollOffset; } } while (mCurrentScrollOffset - mInitialScrollOffset < -gap) { mCurrentScrollOffset += mSelectorElementSize; if (isAscendingOrder()) { incrementSelectorIndices(selectorIndices); } else { decrementSelectorIndices(selectorIndices); } setValueInternal(selectorIndices[mWheelMiddleItemIndex], true); if (!mWrapSelectorWheel && selectorIndices[mWheelMiddleItemIndex] > mMaxValue) { mCurrentScrollOffset = mInitialScrollOffset; } } if (startScrollOffset != mCurrentScrollOffset) { if (isHorizontalMode()) { onScrollChanged(mCurrentScrollOffset, 0, startScrollOffset, 0); } else { onScrollChanged(0, mCurrentScrollOffset, 0, startScrollOffset); } } } private int computeScrollOffset(boolean isHorizontalMode) { return isHorizontalMode ? mCurrentScrollOffset : 0; } private int computeScrollRange(boolean isHorizontalMode) { return isHorizontalMode ? (mMaxValue - mMinValue + 1) * mSelectorElementSize : 0; } private int computeScrollExtent(boolean isHorizontalMode) { return isHorizontalMode ? getWidth() : getHeight(); } @Override protected int computeHorizontalScrollOffset() { return computeScrollOffset(isHorizontalMode()); } @Override protected int computeHorizontalScrollRange() { return computeScrollRange(isHorizontalMode()); } @Override protected int computeHorizontalScrollExtent() { return computeScrollExtent(isHorizontalMode()); } @Override protected int computeVerticalScrollOffset() { return computeScrollOffset(!isHorizontalMode()); } @Override protected int computeVerticalScrollRange() { return computeScrollRange(!isHorizontalMode()); } @Override protected int computeVerticalScrollExtent() { return computeScrollExtent(isHorizontalMode()); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mNumberFormatter = NumberFormat.getInstance(); } /** * Set listener to be notified on click of the current value. * * @param onClickListener The listener. */ public void setOnClickListener(OnClickListener onClickListener) { mOnClickListener = onClickListener; } /** * Sets the listener to be notified on change of the current value. * * @param onValueChangedListener The listener. */ public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) { mOnValueChangeListener = onValueChangedListener; } /** * Set listener to be notified for scroll state changes. * * @param onScrollListener The listener. */ public void setOnScrollListener(OnScrollListener onScrollListener) { mOnScrollListener = onScrollListener; } /** * Set the formatter to be used for formatting the current value. * <p> * Note: If you have provided alternative values for the values this * formatter is never invoked. * </p> * * @param formatter The formatter object. If formatter is <code>null</code>, * {@link String#valueOf(int)} will be used. * @see #setDisplayedValues(String[]) */ public void setFormatter(Formatter formatter) { if (formatter == mFormatter) { return; } mFormatter = formatter; initializeSelectorWheelIndices(); updateInputTextView(); } /** * Set the current value for the number picker. * <p> * If the argument is less than the {@link NumberPicker#getMinValue()} and * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the * current value is set to the {@link NumberPicker#getMinValue()} value. * </p> * <p> * If the argument is less than the {@link NumberPicker#getMinValue()} and * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the * current value is set to the {@link NumberPicker#getMaxValue()} value. * </p> * <p> * If the argument is less than the {@link NumberPicker#getMaxValue()} and * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the * current value is set to the {@link NumberPicker#getMaxValue()} value. * </p> * <p> * If the argument is less than the {@link NumberPicker#getMaxValue()} and * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the * current value is set to the {@link NumberPicker#getMinValue()} value. * </p> * * @param value The current value. * @see #setWrapSelectorWheel(boolean) * @see #setMinValue(int) * @see #setMaxValue(int) */ public void setValue(int value) { setValueInternal(value, false); } private float getMaxTextSize() { return Math.max(mTextSize, mSelectedTextSize); } private float getPaintCenterY(Paint.FontMetrics fontMetrics) { if (fontMetrics == null) { return 0; } return Math.abs(fontMetrics.top + fontMetrics.bottom) / 2; } /** * Computes the max width if no such specified as an attribute. */ private void tryComputeMaxWidth() { if (!mComputeMaxWidth) { return; } mSelectorWheelPaint.setTextSize(getMaxTextSize()); int maxTextWidth = 0; if (mDisplayedValues == null) { float maxDigitWidth = 0; for (int i = 0; i <= 9; i++) { final float digitWidth = mSelectorWheelPaint.measureText(formatNumber(i)); if (digitWidth > maxDigitWidth) { maxDigitWidth = digitWidth; } } int numberOfDigits = 0; int current = mMaxValue; while (current > 0) { numberOfDigits++; current = current / 10; } maxTextWidth = (int) (numberOfDigits * maxDigitWidth); } else { for (String displayedValue : mDisplayedValues) { final float textWidth = mSelectorWheelPaint.measureText(displayedValue); if (textWidth > maxTextWidth) { maxTextWidth = (int) textWidth; } } } maxTextWidth += mSelectedText.getPaddingLeft() + mSelectedText.getPaddingRight(); if (mMaxWidth != maxTextWidth) { mMaxWidth = Math.max(maxTextWidth, mMinWidth); invalidate(); } } /** * Gets whether the selector wheel wraps when reaching the min/max value. * * @return True if the selector wheel wraps. * @see #getMinValue() * @see #getMaxValue() */ public boolean getWrapSelectorWheel() { return mWrapSelectorWheel; } /** * Sets whether the selector wheel shown during flinging/scrolling should * wrap around the {@link NumberPicker#getMinValue()} and * {@link NumberPicker#getMaxValue()} values. * <p> * By default if the range (max - min) is more than the number of items shown * on the selector wheel the selector wheel wrapping is enabled. * </p> * <p> * <strong>Note:</strong> If the number of items, i.e. the range ( * {@link #getMaxValue()} - {@link #getMinValue()}) is less than * the number of items shown on the selector wheel, the selector wheel will * not wrap. Hence, in such a case calling this method is a NOP. * </p> * * @param wrapSelectorWheel Whether to wrap. */ public void setWrapSelectorWheel(boolean wrapSelectorWheel) { mWrapSelectorWheelPreferred = wrapSelectorWheel; updateWrapSelectorWheel(); } /** * Whether or not the selector wheel should be wrapped is determined by user choice and whether * the choice is allowed. The former comes from {@link #setWrapSelectorWheel(boolean)}, the * latter is calculated based on min & max value set vs selector's visual length. Therefore, * this method should be called any time any of the 3 values (i.e. user choice, min and max * value) gets updated. */ private void updateWrapSelectorWheel() { mWrapSelectorWheel = isWrappingAllowed() && mWrapSelectorWheelPreferred; } private boolean isWrappingAllowed() { return mMaxValue - mMinValue >= mSelectorIndices.length - 1; } /** * Sets the speed at which the numbers be incremented and decremented when * the up and down buttons are long pressed respectively. * <p> * The default value is 300 ms. * </p> * * @param intervalMillis The speed (in milliseconds) at which the numbers * will be incremented and decremented. */ public void setOnLongPressUpdateInterval(long intervalMillis) { mLongPressUpdateInterval = intervalMillis; } /** * Returns the value of the picker. * * @return The value. */ public int getValue() { return mValue; } /** * Returns the min value of the picker. * * @return The min value */ public int getMinValue() { return mMinValue; } /** * Sets the min value of the picker. * * @param minValue The min value inclusive. * * <strong>Note:</strong> The length of the displayed values array * set via {@link #setDisplayedValues(String[])} must be equal to the * range of selectable numbers which is equal to * {@link #getMaxValue()} - {@link #getMinValue()} + 1. */ public void setMinValue(int minValue) { // if (minValue < 0) { // throw new IllegalArgumentException("minValue must be >= 0"); // } mMinValue = minValue; if (mMinValue > mValue) { mValue = mMinValue; } updateWrapSelectorWheel(); initializeSelectorWheelIndices(); updateInputTextView(); tryComputeMaxWidth(); invalidate(); } /** * Returns the max value of the picker. * * @return The max value. */ public int getMaxValue() { return mMaxValue; } /** * Sets the max value of the picker. * * @param maxValue The max value inclusive. * * <strong>Note:</strong> The length of the displayed values array * set via {@link #setDisplayedValues(String[])} must be equal to the * range of selectable numbers which is equal to * {@link #getMaxValue()} - {@link #getMinValue()} + 1. */ public void setMaxValue(int maxValue) { if (maxValue < 0) { throw new IllegalArgumentException("maxValue must be >= 0"); } mMaxValue = maxValue; if (mMaxValue < mValue) { mValue = mMaxValue; } updateWrapSelectorWheel(); initializeSelectorWheelIndices(); updateInputTextView(); tryComputeMaxWidth(); invalidate(); } /** * Gets the values to be displayed instead of string values. * * @return The displayed values. */ public String[] getDisplayedValues() { return mDisplayedValues; } /** * Sets the values to be displayed. * * @param displayedValues The displayed values. * * <strong>Note:</strong> The length of the displayed values array * must be equal to the range of selectable numbers which is equal to * {@link #getMaxValue()} - {@link #getMinValue()} + 1. */ public void setDisplayedValues(String[] displayedValues) { if (mDisplayedValues == displayedValues) { return; } mDisplayedValues = displayedValues; if (mDisplayedValues != null) { // Allow text entry rather than strictly numeric entry. mSelectedText.setRawInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); } else { mSelectedText.setRawInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); } updateInputTextView(); initializeSelectorWheelIndices(); tryComputeMaxWidth(); } private float getFadingEdgeStrength(boolean isHorizontalMode) { return isHorizontalMode && mFadingEdgeEnabled ? mFadingEdgeStrength : 0; } @Override protected float getTopFadingEdgeStrength() { return getFadingEdgeStrength(!isHorizontalMode()); } @Override protected float getBottomFadingEdgeStrength() { return getFadingEdgeStrength(!isHorizontalMode()); } @Override protected float getLeftFadingEdgeStrength() { return getFadingEdgeStrength(isHorizontalMode()); } @Override protected float getRightFadingEdgeStrength() { return getFadingEdgeStrength(isHorizontalMode()); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); removeAllCallbacks(); } @CallSuper @Override protected void drawableStateChanged() { super.drawableStateChanged(); if (mDividerDrawable != null && mDividerDrawable.isStateful() && mDividerDrawable.setState(getDrawableState())) { invalidateDrawable(mDividerDrawable); } } @CallSuper @Override public void jumpDrawablesToCurrentState() { super.jumpDrawablesToCurrentState(); if (mDividerDrawable != null) { mDividerDrawable.jumpToCurrentState(); } } @Override protected void onDraw(Canvas canvas) { // save canvas canvas.save(); final boolean showSelectorWheel = !mHideWheelUntilFocused || hasFocus(); float x, y; if (isHorizontalMode()) { x = mCurrentScrollOffset; y = mSelectedText.getBaseline() + mSelectedText.getTop(); if (mRealWheelItemCount < DEFAULT_WHEEL_ITEM_COUNT) { canvas.clipRect(mLeftDividerLeft, 0, mRightDividerRight, getBottom()); } } else { x = (getRight() - getLeft()) / 2f; y = mCurrentScrollOffset; if (mRealWheelItemCount < DEFAULT_WHEEL_ITEM_COUNT) { canvas.clipRect(0, mTopDividerTop, getRight(), mBottomDividerBottom); } } // draw the selector wheel int[] selectorIndices = getSelectorIndices(); for (int i = 0; i < selectorIndices.length; i++) { int selectorIndex = selectorIndices[isAscendingOrder() ? i : selectorIndices.length - i - 1]; String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); if (i == mWheelMiddleItemIndex) { mSelectorWheelPaint.setTextAlign(Paint.Align.values()[mSelectedTextAlign]); mSelectorWheelPaint.setTextSize(mSelectedTextSize); mSelectorWheelPaint.setColor(mSelectedTextColor); mSelectorWheelPaint.setFakeBoldText(selectedTextBold); mSelectorWheelPaint.setStrikeThruText(mSelectedTextStrikeThru); mSelectorWheelPaint.setUnderlineText(mSelectedTextUnderline); mSelectorWheelPaint.setTypeface(mSelectedTypeface); scrollSelectorValue += label; } else { mSelectorWheelPaint.setTextAlign(Paint.Align.values()[mTextAlign]); mSelectorWheelPaint.setTextSize(mTextSize); mSelectorWheelPaint.setColor(mTextColor); mSelectorWheelPaint.setFakeBoldText(textBold); mSelectorWheelPaint.setStrikeThruText(mTextStrikeThru); mSelectorWheelPaint.setUnderlineText(mTextUnderline); mSelectorWheelPaint.setTypeface(mTypeface); scrollSelectorValue = scrollSelectorValue.replace(label, ""); } if (scrollSelectorValue == null) { continue; } // Do not draw the middle item if input is visible since the input // is shown only if the wheel is static and it covers the middle // item. Otherwise, if the user starts editing the text via the // IME he may see a dimmed version of the old value intermixed // with the new one. if ((showSelectorWheel && i != mWheelMiddleItemIndex) || (i == mWheelMiddleItemIndex && mSelectedText.getVisibility() != VISIBLE)) { float textY = y; if (!isHorizontalMode()) { textY += getPaintCenterY(mSelectorWheelPaint.getFontMetrics()); } int xOffset = 0; int yOffset = 0; if (i != mWheelMiddleItemIndex && mItemSpacing != 0) { if (isHorizontalMode()) { if (i > mWheelMiddleItemIndex) { xOffset = mItemSpacing; } else { xOffset = -mItemSpacing; } } else { if (i > mWheelMiddleItemIndex) { yOffset = mItemSpacing; } else { yOffset = -mItemSpacing; } } } drawText(scrollSelectorValue, x + xOffset, textY + yOffset, mSelectorWheelPaint, canvas); } if (isHorizontalMode()) { x += mSelectorElementSize; } else { y += mSelectorElementSize; } } // restore canvas canvas.restore(); // draw the dividers if (showSelectorWheel && mDividerDrawable != null) { if (isHorizontalMode()) drawHorizontalDividers(canvas); else drawVerticalDividers(canvas); } } private void drawHorizontalDividers(Canvas canvas) { switch (mDividerType) { case SIDE_LINES: final int top; final int bottom; if (mDividerLength > 0 && mDividerLength <= mMaxHeight) { top = (mMaxHeight - mDividerLength) / 2; bottom = top + mDividerLength; } else { top = 0; bottom = getBottom(); } // draw the left divider final int leftOfLeftDivider = mLeftDividerLeft; final int rightOfLeftDivider = leftOfLeftDivider + mDividerThickness; mDividerDrawable.setBounds(leftOfLeftDivider, top, rightOfLeftDivider, bottom); mDividerDrawable.draw(canvas); // draw the right divider final int rightOfRightDivider = mRightDividerRight; final int leftOfRightDivider = rightOfRightDivider - mDividerThickness; mDividerDrawable.setBounds(leftOfRightDivider, top, rightOfRightDivider, bottom); mDividerDrawable.draw(canvas); break; case UNDERLINE: final int left; final int right; if (mDividerLength > 0 && mDividerLength <= mMaxWidth) { left = (mMaxWidth - mDividerLength) / 2; right = left + mDividerLength; } else { left = mLeftDividerLeft; right = mRightDividerRight; } final int bottomOfUnderlineDivider = mBottomDividerBottom; final int topOfUnderlineDivider = bottomOfUnderlineDivider - mDividerThickness; mDividerDrawable.setBounds( left, topOfUnderlineDivider, right, bottomOfUnderlineDivider ); mDividerDrawable.draw(canvas); break; } } private void drawVerticalDividers(Canvas canvas) { final int left; final int right; if (mDividerLength > 0 && mDividerLength <= mMaxWidth) { left = (mMaxWidth - mDividerLength) / 2; right = left + mDividerLength; } else { left = 0; right = getRight(); } switch (mDividerType) { case SIDE_LINES: // draw the top divider final int topOfTopDivider = mTopDividerTop; final int bottomOfTopDivider = topOfTopDivider + mDividerThickness; mDividerDrawable.setBounds(left, topOfTopDivider, right, bottomOfTopDivider); mDividerDrawable.draw(canvas); // draw the bottom divider final int bottomOfBottomDivider = mBottomDividerBottom; final int topOfBottomDivider = bottomOfBottomDivider - mDividerThickness; mDividerDrawable.setBounds( left, topOfBottomDivider, right, bottomOfBottomDivider); mDividerDrawable.draw(canvas); break; case UNDERLINE: final int bottomOfUnderlineDivider = mBottomDividerBottom; final int topOfUnderlineDivider = bottomOfUnderlineDivider - mDividerThickness; mDividerDrawable.setBounds( left, topOfUnderlineDivider, right, bottomOfUnderlineDivider ); mDividerDrawable.draw(canvas); break; } } private void drawText(String text, float x, float y, Paint paint, Canvas canvas) { if (text.contains("\n")) { final String[] lines = text.split("\n"); final float height = Math.abs(paint.descent() + paint.ascent()) * mLineSpacingMultiplier; final float diff = (lines.length - 1) * height / 2; y -= diff; for (String line : lines) { canvas.drawText(line, x, y, paint); y += height; } } else { canvas.drawText(text, x, y, paint); } } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); event.setClassName(NumberPicker.class.getName()); event.setScrollable(isScrollerEnabled()); final int scroll = (mMinValue + mValue) * mSelectorElementSize; final int maxScroll = (mMaxValue - mMinValue) * mSelectorElementSize; if (isHorizontalMode()) { event.setScrollX(scroll); event.setMaxScrollX(maxScroll); } else { event.setScrollY(scroll); event.setMaxScrollY(maxScroll); } } /** * Makes a measure spec that tries greedily to use the max value. * * @param measureSpec The measure spec. * @param maxSize The max value for the size. * @return A measure spec greedily imposing the max size. */ private int makeMeasureSpec(int measureSpec, int maxSize) { if (maxSize == SIZE_UNSPECIFIED) { return measureSpec; } final int size = MeasureSpec.getSize(measureSpec); final int mode = MeasureSpec.getMode(measureSpec); switch (mode) { case MeasureSpec.EXACTLY: return measureSpec; case MeasureSpec.AT_MOST: return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY); case MeasureSpec.UNSPECIFIED: return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY); default: throw new IllegalArgumentException("Unknown measure mode: " + mode); } } /** * Utility to reconcile a desired size and state, with constraints imposed * by a MeasureSpec. Tries to respect the min size, unless a different size * is imposed by the constraints. * * @param minSize The minimal desired size. * @param measuredSize The currently measured size. * @param measureSpec The current measure spec. * @return The resolved size and state. */ private int resolveSizeAndStateRespectingMinSize(int minSize, int measuredSize, int measureSpec) { if (minSize != SIZE_UNSPECIFIED) { final int desiredWidth = Math.max(minSize, measuredSize); return resolveSizeAndState(desiredWidth, measureSpec, 0); } else { return measuredSize; } } /** * Utility to reconcile a desired size and state, with constraints imposed * by a MeasureSpec. Will take the desired size, unless a different size * is imposed by the constraints. The returned value is a compound integer, * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting * size is smaller than the size the view wants to be. * * @param size How big the view wants to be * @param measureSpec Constraints imposed by the parent * @return Size information bit mask as defined by * {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}. */ public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: if (specSize < size) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; case MeasureSpec.EXACTLY: result = specSize; break; } return result | (childMeasuredState & MEASURED_STATE_MASK); } /** * Resets the selector indices and clear the cached string representation of * these indices. */ private void initializeSelectorWheelIndices() { mSelectorIndexToStringCache.clear(); int[] selectorIndices = getSelectorIndices(); int current = getValue(); for (int i = 0; i < selectorIndices.length; i++) { int selectorIndex = current + (i - mWheelMiddleItemIndex); if (mWrapSelectorWheel) { selectorIndex = getWrappedSelectorIndex(selectorIndex); } selectorIndices[i] = selectorIndex; ensureCachedScrollSelectorValue(selectorIndices[i]); } } /** * Sets the current value of this NumberPicker. * * @param current The new value of the NumberPicker. * @param notifyChange Whether to notify if the current value changed. */ private void setValueInternal(int current, boolean notifyChange) { if (mValue == current) { return; } // Wrap around the values if we go past the start or end if (mWrapSelectorWheel) { current = getWrappedSelectorIndex(current); } else { current = Math.max(current, mMinValue); current = Math.min(current, mMaxValue); } int previous = mValue; mValue = current; // If we're flinging, we'll update the text view at the end when it becomes visible if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) { updateInputTextView(); } if (notifyChange) { notifyChange(previous, current); } initializeSelectorWheelIndices(); updateAccessibilityDescription(); invalidate(); } /** * Updates the accessibility values of the view, * to the currently selected value */ private void updateAccessibilityDescription() { if (!mAccessibilityDescriptionEnabled) { return; } this.setContentDescription(String.valueOf(getValue())); } /** * Changes the current value by one which is increment or * decrement based on the passes argument. * decrement the current value. * * @param increment True to increment, false to decrement. */ private void changeValueByOne(boolean increment) { if (!moveToFinalScrollerPosition(mFlingScroller)) { moveToFinalScrollerPosition(mAdjustScroller); } smoothScroll(increment, 1); } /** * Starts a smooth scroll to wheel position. * * @param position The wheel position to scroll to. */ public void smoothScrollToPosition(int position) { final int currentPosition = getSelectorIndices()[mWheelMiddleItemIndex]; if (currentPosition == position) { return; } smoothScroll(position > currentPosition, Math.abs(position - currentPosition)); } /** * Starts a smooth scroll * * @param increment True to increment, false to decrement. * @param steps The steps to scroll. */ public void smoothScroll(boolean increment, int steps) { final int diffSteps = (increment ? -mSelectorElementSize : mSelectorElementSize) * steps; if (isHorizontalMode()) { mPreviousScrollerX = 0; mFlingScroller.startScroll(0, 0, diffSteps, 0, SNAP_SCROLL_DURATION); } else { mPreviousScrollerY = 0; mFlingScroller.startScroll(0, 0, 0, diffSteps, SNAP_SCROLL_DURATION); } invalidate(); } private void initializeSelectorWheel() { initializeSelectorWheelIndices(); int[] selectorIndices = getSelectorIndices(); int totalTextSize = (int) ((selectorIndices.length - 1) * mTextSize + mSelectedTextSize); float textGapCount = selectorIndices.length; if (isHorizontalMode()) { float totalTextGapWidth = (getRight() - getLeft()) - totalTextSize; mSelectorTextGapWidth = (int) (totalTextGapWidth / textGapCount); mSelectorElementSize = (int) getMaxTextSize() + mSelectorTextGapWidth; mInitialScrollOffset = (int) (mSelectedTextCenterX - mSelectorElementSize * mWheelMiddleItemIndex); } else { float totalTextGapHeight = (getBottom() - getTop()) - totalTextSize; mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount); mSelectorElementSize = (int) getMaxTextSize() + mSelectorTextGapHeight; mInitialScrollOffset = (int) (mSelectedTextCenterY - mSelectorElementSize * mWheelMiddleItemIndex); } mCurrentScrollOffset = mInitialScrollOffset; updateInputTextView(); } private void initializeFadingEdges() { if (isHorizontalMode()) { setHorizontalFadingEdgeEnabled(true); setVerticalFadingEdgeEnabled(false); setFadingEdgeLength((getRight() - getLeft() - (int) mTextSize) / 2); } else { setHorizontalFadingEdgeEnabled(false); setVerticalFadingEdgeEnabled(true); setFadingEdgeLength((getBottom() - getTop() - (int) mTextSize) / 2); } } /** * Callback invoked upon completion of a given <code>scroller</code>. */ private void onScrollerFinished(Scroller scroller) { if (scroller == mFlingScroller) { ensureScrollWheelAdjusted(); updateInputTextView(); onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } else if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { updateInputTextView(); } } /** * Handles transition to a given <code>scrollState</code> */ private void onScrollStateChange(int scrollState) { if (mScrollState == scrollState) { return; } mScrollState = scrollState; if (mOnScrollListener != null) { mOnScrollListener.onScrollStateChange(this, scrollState); } } /** * Flings the selector with the given <code>velocity</code>. */ private void fling(int velocity) { if (isHorizontalMode()) { mPreviousScrollerX = 0; if (velocity > 0) { mFlingScroller.fling(0, 0, velocity, 0, 0, Integer.MAX_VALUE, 0, 0); } else { mFlingScroller.fling(Integer.MAX_VALUE, 0, velocity, 0, 0, Integer.MAX_VALUE, 0, 0); } } else { mPreviousScrollerY = 0; if (velocity > 0) { mFlingScroller.fling(0, 0, 0, velocity, 0, 0, 0, Integer.MAX_VALUE); } else { mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocity, 0, 0, 0, Integer.MAX_VALUE); } } invalidate(); } /** * @return The wrapped index <code>selectorIndex</code> value. */ private int getWrappedSelectorIndex(int selectorIndex) { if (selectorIndex > mMaxValue) { return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1; } else if (selectorIndex < mMinValue) { return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1; } return selectorIndex; } private int[] getSelectorIndices() { return mSelectorIndices; } /** * Increments the <code>selectorIndices</code> whose string representations * will be displayed in the selector. */ private void incrementSelectorIndices(int[] selectorIndices) { for (int i = 0; i < selectorIndices.length - 1; i++) { selectorIndices[i] = selectorIndices[i + 1]; } int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1; if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) { nextScrollSelectorIndex = mMinValue; } selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex; ensureCachedScrollSelectorValue(nextScrollSelectorIndex); } /** * Decrements the <code>selectorIndices</code> whose string representations * will be displayed in the selector. */ private void decrementSelectorIndices(int[] selectorIndices) { for (int i = selectorIndices.length - 1; i > 0; i--) { selectorIndices[i] = selectorIndices[i - 1]; } int nextScrollSelectorIndex = selectorIndices[1] - 1; if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) { nextScrollSelectorIndex = mMaxValue; } selectorIndices[0] = nextScrollSelectorIndex; ensureCachedScrollSelectorValue(nextScrollSelectorIndex); } /** * Ensures we have a cached string representation of the given <code> * selectorIndex</code> to avoid multiple instantiations of the same string. */ private void ensureCachedScrollSelectorValue(int selectorIndex) { SparseArray<String> cache = mSelectorIndexToStringCache; String scrollSelectorValue = cache.get(selectorIndex); if (scrollSelectorValue != null) { return; } if (selectorIndex < mMinValue || selectorIndex > mMaxValue) { scrollSelectorValue = ""; } else { if (mDisplayedValues != null) { int displayedValueIndex = selectorIndex - mMinValue; if (displayedValueIndex >= mDisplayedValues.length) { cache.remove(selectorIndex); return; } scrollSelectorValue = mDisplayedValues[displayedValueIndex]; } else { scrollSelectorValue = formatNumber(selectorIndex); } } cache.put(selectorIndex, scrollSelectorValue); } private String formatNumber(int value) { return (mFormatter != null) ? mFormatter.format(value) : formatNumberWithLocale(value); } /** * Updates the view of this NumberPicker. If displayValues were specified in * the string corresponding to the index specified by the current value will * be returned. Otherwise, the formatter specified in {@link #setFormatter} * will be used to format the number. */ private void updateInputTextView() { /* * If we don't have displayed values then use the current number else * find the correct value in the displayed values for the current * number. */ String text = (mDisplayedValues == null) ? formatNumber(mValue) : mDisplayedValues[mValue - mMinValue]; if (TextUtils.isEmpty(text)) { return; } CharSequence beforeText = mSelectedText.getText(); if (text.equals(beforeText.toString())) { return; } mSelectedText.setText(text + label); } /** * Notifies the listener, if registered, of a change of the value of this * NumberPicker. */ private void notifyChange(int previous, int current) { if (mOnValueChangeListener != null) { mOnValueChangeListener.onValueChange(this, previous, current); } } /** * Posts a command for changing the current value by one. * * @param increment Whether to increment or decrement the value. */ private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) { if (mChangeCurrentByOneFromLongPressCommand == null) { mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand(); } else { removeCallbacks(mChangeCurrentByOneFromLongPressCommand); } mChangeCurrentByOneFromLongPressCommand.setStep(increment); postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis); } /** * Posts a command for changing the current value by one. * * @param increment Whether to increment or decrement the value. */ private void postChangeCurrentByOneFromLongPress(boolean increment) { postChangeCurrentByOneFromLongPress(increment, ViewConfiguration.getLongPressTimeout()); } /** * Removes the command for changing the current value by one. */ private void removeChangeCurrentByOneFromLongPress() { if (mChangeCurrentByOneFromLongPressCommand != null) { removeCallbacks(mChangeCurrentByOneFromLongPressCommand); } } /** * Removes all pending callback from the message queue. */ private void removeAllCallbacks() { if (mChangeCurrentByOneFromLongPressCommand != null) { removeCallbacks(mChangeCurrentByOneFromLongPressCommand); } if (mSetSelectionCommand != null) { mSetSelectionCommand.cancel(); } } /** * @return The selected index given its displayed <code>value</code>. */ private int getSelectedPos(String value) { if (mDisplayedValues == null) { try { return Integer.parseInt(value); } catch (NumberFormatException e) { // Ignore as if it's not a number we don't care } } else { for (int i = 0; i < mDisplayedValues.length; i++) { // Don't force the user to type in jan when ja will do value = value.toLowerCase(); if (mDisplayedValues[i].toLowerCase().startsWith(value)) { return mMinValue + i; } } /* * The user might have typed in a number into the month field i.e. * 10 instead of OCT so support that too. */ try { return Integer.parseInt(value); } catch (NumberFormatException e) { // Ignore as if it's not a number we don't care } } return mMinValue; } /** * Posts a {@link SetSelectionCommand} from the given * {@code selectionStart} to {@code selectionEnd}. */ private void postSetSelectionCommand(int selectionStart, int selectionEnd) { if (mSetSelectionCommand == null) { mSetSelectionCommand = new SetSelectionCommand(mSelectedText); } else { mSetSelectionCommand.post(selectionStart, selectionEnd); } } /** * The numbers accepted by the input text's {@link Filter} */ private static final char[] DIGIT_CHARACTERS = new char[]{ // Latin digits are the common case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // Arabic-Indic '\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668', '\u0669', // Extended Arabic-Indic '\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5', '\u06f6', '\u06f7', '\u06f8', '\u06f9', // Hindi and Marathi (Devanagari script) '\u0966', '\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e', '\u096f', // Bengali '\u09e6', '\u09e7', '\u09e8', '\u09e9', '\u09ea', '\u09eb', '\u09ec', '\u09ed', '\u09ee', '\u09ef', // Kannada '\u0ce6', '\u0ce7', '\u0ce8', '\u0ce9', '\u0cea', '\u0ceb', '\u0cec', '\u0ced', '\u0cee', '\u0cef', // Negative '-' }; /** * Filter for accepting only valid indices or prefixes of the string * representation of valid indices. */ class InputTextFilter extends NumberKeyListener { // XXX This doesn't allow for range limits when controlled by a soft input method! public int getInputType() { return InputType.TYPE_CLASS_TEXT; } @Override protected char[] getAcceptedChars() { return DIGIT_CHARACTERS; } @Override public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { // We don't know what the output will be, so always cancel any // pending set selection command. if (mSetSelectionCommand != null) { mSetSelectionCommand.cancel(); } if (mDisplayedValues == null) { CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); if (filtered == null) { filtered = source.subSequence(start, end); } String result = String.valueOf(dest.subSequence(0, dstart)) + filtered + dest.subSequence(dend, dest.length()); if ("".equals(result)) { return result; } int val = getSelectedPos(result); /* * Ensure the user can't type in a value greater than the max * allowed. We have to allow less than min as the user might * want to delete some numbers and then type a new number. * And prevent multiple-"0" that exceeds the length of upper * bound number. */ if (val > mMaxValue || result.length() > String.valueOf(mMaxValue).length()) { return ""; } else { return filtered; } } else { CharSequence filtered = String.valueOf(source.subSequence(start, end)); if (TextUtils.isEmpty(filtered)) { return ""; } String result = String.valueOf(dest.subSequence(0, dstart)) + filtered + dest.subSequence(dend, dest.length()); String str = String.valueOf(result).toLowerCase(); for (String val : mDisplayedValues) { String valLowerCase = val.toLowerCase(); if (valLowerCase.startsWith(str)) { postSetSelectionCommand(result.length(), val.length()); return val.subSequence(dstart, val.length()); } } return ""; } } } /** * Ensures that the scroll wheel is adjusted i.e. there is no offset and the * middle element is in the middle of the widget. */ private void ensureScrollWheelAdjusted() { // adjust to the closest value int delta = mInitialScrollOffset - mCurrentScrollOffset; if (delta == 0) { return; } if (Math.abs(delta) > mSelectorElementSize / 2) { delta += (delta > 0) ? -mSelectorElementSize : mSelectorElementSize; } if (isHorizontalMode()) { mPreviousScrollerX = 0; mAdjustScroller.startScroll(0, 0, delta, 0, SELECTOR_ADJUSTMENT_DURATION_MILLIS); } else { mPreviousScrollerY = 0; mAdjustScroller.startScroll(0, 0, 0, delta, SELECTOR_ADJUSTMENT_DURATION_MILLIS); } invalidate(); } /** * Command for setting the input text selection. */ private static class SetSelectionCommand implements Runnable { private final EditText mInputText; private int mSelectionStart; private int mSelectionEnd; /** * Whether this runnable is currently posted. */ private boolean mPosted; SetSelectionCommand(EditText inputText) { mInputText = inputText; } void post(int selectionStart, int selectionEnd) { mSelectionStart = selectionStart; mSelectionEnd = selectionEnd; if (!mPosted) { mInputText.post(this); mPosted = true; } } void cancel() { if (mPosted) { mInputText.removeCallbacks(this); mPosted = false; } } @Override public void run() { mPosted = false; mInputText.setSelection(mSelectionStart, mSelectionEnd); } } /** * Command for changing the current value from a long press by one. */ class ChangeCurrentByOneFromLongPressCommand implements Runnable { private boolean mIncrement; private void setStep(boolean increment) { mIncrement = increment; } @Override public void run() { changeValueByOne(mIncrement); postDelayed(this, mLongPressUpdateInterval); } } private String formatNumberWithLocale(int value) { // return mNumberFormatter.format(value); return value + ""; } private float dpToPx(float dp) { return dp * getResources().getDisplayMetrics().density; } private float pxToDp(float px) { return px / getResources().getDisplayMetrics().density; } private float spToPx(float sp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics()); } private float pxToSp(float px) { return px / getResources().getDisplayMetrics().scaledDensity; } private Formatter stringToFormatter(final String formatter) { if (TextUtils.isEmpty(formatter)) { return null; } return new Formatter() { @Override public String format(int i) { return String.format(Locale.getDefault(), formatter, i); } }; } private void setWidthAndHeight() { if (isHorizontalMode()) { mMinHeight = SIZE_UNSPECIFIED; mMaxHeight = (int) dpToPx(DEFAULT_MIN_WIDTH); mMinWidth = (int) dpToPx(DEFAULT_MAX_HEIGHT); mMaxWidth = SIZE_UNSPECIFIED; } else { mMinHeight = SIZE_UNSPECIFIED; mMaxHeight = (int) dpToPx(DEFAULT_MAX_HEIGHT); mMinWidth = (int) dpToPx(DEFAULT_MIN_WIDTH); mMaxWidth = SIZE_UNSPECIFIED; } } public void setAccessibilityDescriptionEnabled(boolean enabled) { mAccessibilityDescriptionEnabled = enabled; } public void setDividerColor(@ColorInt int color) { mDividerColor = color; mDividerDrawable = new ColorDrawable(color); invalidate(); } public void setDividerColorResource(@ColorRes int colorId) { setDividerColor(ContextCompat.getColor(mContext, colorId)); } public void setDividerDistance(int distance) { mDividerDistance = distance; } public void setDividerDistanceResource(@DimenRes int dimenId) { setDividerDistance(getResources().getDimensionPixelSize(dimenId)); } public void setDividerType(@DividerType int dividerType) { mDividerType = dividerType; invalidate(); } public void setDividerThickness(int thickness) { mDividerThickness = thickness; } public void setDividerThicknessResource(@DimenRes int dimenId) { setDividerThickness(getResources().getDimensionPixelSize(dimenId)); } /** * Should sort numbers in ascending or descending order. * * @param order Pass {@link #ASCENDING} or {@link #ASCENDING}. * Default value is {@link #DESCENDING}. */ public void setOrder(@Order int order) { mOrder = order; } public void setOrientation(@Orientation int orientation) { mOrientation = orientation; setWidthAndHeight(); requestLayout(); } public void setWheelItemCount(int count) { if (count < 1) { throw new IllegalArgumentException("Wheel item count must be >= 1"); } mRealWheelItemCount = count; mWheelItemCount = Math.max(count, DEFAULT_WHEEL_ITEM_COUNT); mWheelMiddleItemIndex = mWheelItemCount / 2; mSelectorIndices = new int[mWheelItemCount]; } public void setFormatter(final String formatter) { if (TextUtils.isEmpty(formatter)) { return; } setFormatter(stringToFormatter(formatter)); } public void setFormatter(@StringRes int stringId) { setFormatter(getResources().getString(stringId)); } public void setFadingEdgeEnabled(boolean fadingEdgeEnabled) { mFadingEdgeEnabled = fadingEdgeEnabled; } public void setFadingEdgeStrength(float strength) { mFadingEdgeStrength = strength; } public void setScrollerEnabled(boolean scrollerEnabled) { mScrollerEnabled = scrollerEnabled; } public void setSelectedTextAlign(@Align int align) { mSelectedTextAlign = align; } public void setSelectedTextColor(@ColorInt int color) { mSelectedTextColor = color; mSelectedText.setTextColor(mSelectedTextColor); invalidate(); } public void setSelectedTextColorResource(@ColorRes int colorId) { setSelectedTextColor(ContextCompat.getColor(mContext, colorId)); } public void setSelectedTextSize(float textSize) { mSelectedTextSize = textSize; mSelectedText.setTextSize(pxToSp(mSelectedTextSize)); invalidate(); } public void setSelectedTextSize(@DimenRes int dimenId) { setSelectedTextSize(getResources().getDimension(dimenId)); } public void setSelectedTextStrikeThru(boolean strikeThruText) { mSelectedTextStrikeThru = strikeThruText; } public void setSelectedTextUnderline(boolean underlineText) { mSelectedTextUnderline = underlineText; } public void setSelectedTypeface(Typeface typeface) { mSelectedTypeface = typeface; if (mSelectedTypeface != null) { mSelectorWheelPaint.setTypeface(mSelectedTypeface); } else if (mTypeface != null) { mSelectorWheelPaint.setTypeface(mTypeface); } else { mSelectorWheelPaint.setTypeface(Typeface.MONOSPACE); } invalidate(); } public void setSelectedTypeface(String string, int style) { if (TextUtils.isEmpty(string)) { return; } setSelectedTypeface(Typeface.create(string, style)); } public void setSelectedTypeface(String string) { setSelectedTypeface(string, Typeface.NORMAL); } public void setSelectedTypeface(@StringRes int stringId, int style) { setSelectedTypeface(getResources().getString(stringId), style); } public void setSelectedTypeface(@StringRes int stringId) { setSelectedTypeface(stringId, Typeface.NORMAL); } public void setTextAlign(@Align int align) { mTextAlign = align; } public void setTextColor(@ColorInt int color) { mTextColor = color; mSelectorWheelPaint.setColor(mTextColor); invalidate(); } public void setTextColorResource(@ColorRes int colorId) { setTextColor(ContextCompat.getColor(mContext, colorId)); } public void setTextSize(float textSize) { mTextSize = textSize; mSelectorWheelPaint.setTextSize(mTextSize); invalidate(); } public void setTextSize(@DimenRes int dimenId) { setTextSize(getResources().getDimension(dimenId)); } public void setTextStrikeThru(boolean strikeThruText) { mTextStrikeThru = strikeThruText; } public void setTextUnderline(boolean underlineText) { mTextUnderline = underlineText; } public void setTypeface(Typeface typeface) { mTypeface = typeface; if (mTypeface != null) { mSelectedText.setTypeface(mTypeface); setSelectedTypeface(mSelectedTypeface); } else { mSelectedText.setTypeface(Typeface.MONOSPACE); } invalidate(); } public void setTypeface(String string, int style) { if (TextUtils.isEmpty(string)) { return; } setTypeface(Typeface.create(string, style)); } public void setTypeface(String string) { setTypeface(string, Typeface.NORMAL); } public void setTypeface(@StringRes int stringId, int style) { setTypeface(getResources().getString(stringId), style); } public void setTypeface(@StringRes int stringId) { setTypeface(stringId, Typeface.NORMAL); } public void setLineSpacingMultiplier(float multiplier) { mLineSpacingMultiplier = multiplier; } public void setMaxFlingVelocityCoefficient(int coefficient) { mMaxFlingVelocityCoefficient = coefficient; mMaximumFlingVelocity = mViewConfiguration.getScaledMaximumFlingVelocity() / mMaxFlingVelocityCoefficient; } public void setItemSpacing(int itemSpacing) { mItemSpacing = itemSpacing; } public boolean isHorizontalMode() { return getOrientation() == HORIZONTAL; } public boolean isAscendingOrder() { return getOrder() == ASCENDING; } public boolean isAccessibilityDescriptionEnabled() { return mAccessibilityDescriptionEnabled; } public int getDividerColor() { return mDividerColor; } public float getDividerDistance() { return pxToDp(mDividerDistance); } public float getDividerThickness() { return pxToDp(mDividerThickness); } public int getOrder() { return mOrder; } public int getOrientation() { return mOrientation; } public int getWheelItemCount() { return mWheelItemCount; } public Formatter getFormatter() { return mFormatter; } public boolean isFadingEdgeEnabled() { return mFadingEdgeEnabled; } public float getFadingEdgeStrength() { return mFadingEdgeStrength; } public boolean isScrollerEnabled() { return mScrollerEnabled; } public int getSelectedTextAlign() { return mSelectedTextAlign; } public int getSelectedTextColor() { return mSelectedTextColor; } public float getSelectedTextSize() { return mSelectedTextSize; } public boolean getSelectedTextStrikeThru() { return mSelectedTextStrikeThru; } public boolean getSelectedTextUnderline() { return mSelectedTextUnderline; } public int getTextAlign() { return mTextAlign; } public int getTextColor() { return mTextColor; } public float getTextSize() { return spToPx(mTextSize); } public boolean getTextStrikeThru() { return mTextStrikeThru; } public boolean getTextUnderline() { return mTextUnderline; } public Typeface getTypeface() { return mTypeface; } public float getLineSpacingMultiplier() { return mLineSpacingMultiplier; } public int getMaxFlingVelocityCoefficient() { return mMaxFlingVelocityCoefficient; } public void setLabel(String label) { this.label = label; } public String getLabel() { return label; } public boolean isTextBold() { return textBold; } public void setTextBold(boolean bold) { this.textBold = bold; } public boolean isSelectedTextBold() { return selectedTextBold; } public void setSelectedTextBold(boolean selectBold) { this.selectedTextBold = selectBold; } } date_time_picker/src/main/java/com/loper7/date_time_picker/number_picker/Scroller.java
New file @@ -0,0 +1,596 @@ package com.loper7.date_time_picker.number_picker; /* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.content.Context; import android.hardware.SensorManager; import android.os.Build; import android.view.ViewConfiguration; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; /** * <p>This class encapsulates scrolling. You can use scrollers ({@link Scroller} * to collect the data you need to produce a scrolling animation—for * example, in response to a fling gesture. Scrollers track scroll offsets * for you over time, but they don't automatically apply those positions * to your view. It's your responsibility to get and apply new coordinates * at a rate that will make the scrolling animation look smooth.</p> * * <p>Here is a simple example:</p> * * <pre> private Scroller mScroller = new Scroller(context); * ... * public void zoomIn() { * // Revert any animation currently in progress * mScroller.forceFinished(true); * // Start scrolling by providing a starting point and * // the distance to travel * mScroller.startScroll(0, 0, 100, 0); * // Invalidate to request a redraw * invalidate(); * }</pre> * * <p>To track the changing positions of the x/y coordinates, use * {@link #computeScrollOffset}. The method returns a boolean to indicate * whether the scroller is finished. If it isn't, it means that a fling or * programmatic pan operation is still in progress. You can use this method to * find the current offsets of the x and y coordinates, for example:</p> * * <pre>if (mScroller.computeScrollOffset()) { * // Get current x and y positions * int currX = mScroller.getCurrX(); * int currY = mScroller.getCurrY(); * ... * }</pre> */ public class Scroller { private final Interpolator mInterpolator; private int mMode; private int mStartX; private int mStartY; private int mFinalX; private int mFinalY; private int mMinX; private int mMaxX; private int mMinY; private int mMaxY; private int mCurrX; private int mCurrY; private long mStartTime; private int mDuration; private float mDurationReciprocal; private float mDeltaX; private float mDeltaY; private boolean mFinished; private boolean mFlywheel; private float mVelocity; private float mCurrVelocity; private int mDistance; private float mFlingFriction = ViewConfiguration.getScrollFriction(); private static final int DEFAULT_DURATION = 250; private static final int SCROLL_MODE = 0; private static final int FLING_MODE = 1; private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) private static final float START_TENSION = 0.5f; private static final float END_TENSION = 1.0f; private static final float P1 = START_TENSION * INFLEXION; private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION); private static final int NB_SAMPLES = 100; private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; private float mDeceleration; private final float mPpi; // A context-specific coefficient adjusted to physical values. private float mPhysicalCoeff; static { float x_min = 0.0f; float y_min = 0.0f; for (int i = 0; i < NB_SAMPLES; i++) { final float alpha = (float) i / NB_SAMPLES; float x_max = 1.0f; float x, tx, coef; while (true) { x = x_min + (x_max - x_min) / 2.0f; coef = 3.0f * x * (1.0f - x); tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x; if (Math.abs(tx - alpha) < 1E-5) break; if (tx > alpha) x_max = x; else x_min = x; } SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x; float y_max = 1.0f; float y, dy; while (true) { y = y_min + (y_max - y_min) / 2.0f; coef = 3.0f * y * (1.0f - y); dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y; if (Math.abs(dy - alpha) < 1E-5) break; if (dy > alpha) y_max = y; else y_min = y; } SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; } SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; } /** * Create a Scroller with the default duration and interpolator. */ public Scroller(Context context) { this(context, null); } /** * Create a Scroller with the specified interpolator. If the interpolator is * null, the default (viscous) interpolator will be used. "Flywheel" behavior will * be in effect for apps targeting Honeycomb or newer. */ public Scroller(Context context, Interpolator interpolator) { this(context, interpolator, context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB); } /** * Create a Scroller with the specified interpolator. If the interpolator is * null, the default (viscous) interpolator will be used. Specify whether or * not to support progressive "flywheel" behavior in flinging. */ public Scroller(Context context, Interpolator interpolator, boolean flywheel) { mFinished = true; if (interpolator == null) { mInterpolator = new ViscousFluidInterpolator(); } else { mInterpolator = interpolator; } mPpi = context.getResources().getDisplayMetrics().density * 160.0f; mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); mFlywheel = flywheel; mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning } /** * The amount of friction applied to flings. The default value * is {@link ViewConfiguration#getScrollFriction}. * * @param friction A scalar dimension-less value representing the coefficient of * friction. */ public final void setFriction(float friction) { mDeceleration = computeDeceleration(friction); mFlingFriction = friction; } private float computeDeceleration(float friction) { return SensorManager.GRAVITY_EARTH // g (m/s^2) * 39.37f // inch/meter * mPpi // pixels per inch * friction; } /** * * Returns whether the scroller has finished scrolling. * * @return True if the scroller has finished scrolling, false otherwise. */ public final boolean isFinished() { return mFinished; } /** * Force the finished field to a particular value. * * @param finished The new finished value. */ public final void forceFinished(boolean finished) { mFinished = finished; } /** * Returns how long the scroll event will take, in milliseconds. * * @return The duration of the scroll in milliseconds. */ public final int getDuration() { return mDuration; } /** * Returns the current X offset in the scroll. * * @return The new X offset as an absolute distance from the origin. */ public final int getCurrX() { return mCurrX; } /** * Returns the current Y offset in the scroll. * * @return The new Y offset as an absolute distance from the origin. */ public final int getCurrY() { return mCurrY; } /** * Returns the current velocity. * * @return The original velocity less the deceleration. Result may be * negative. */ public float getCurrVelocity() { return mMode == FLING_MODE ? mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f; } /** * Returns the start X offset in the scroll. * * @return The start X offset as an absolute distance from the origin. */ public final int getStartX() { return mStartX; } /** * Returns the start Y offset in the scroll. * * @return The start Y offset as an absolute distance from the origin. */ public final int getStartY() { return mStartY; } /** * Returns where the scroll will end. Valid only for "fling" scrolls. * * @return The final X offset as an absolute distance from the origin. */ public final int getFinalX() { return mFinalX; } /** * Returns where the scroll will end. Valid only for "fling" scrolls. * * @return The final Y offset as an absolute distance from the origin. */ public final int getFinalY() { return mFinalY; } /** * Call this when you want to know the new location. If it returns true, * the animation is not yet finished. */ public boolean computeScrollOffset() { if (mFinished) { return false; } int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; case FLING_MODE: final float t = (float) timePassed / mDuration; final int index = (int) (NB_SAMPLES * t); float distanceCoef = 1.f; float velocityCoef = 0.f; if (index < NB_SAMPLES) { final float t_inf = (float) index / NB_SAMPLES; final float t_sup = (float) (index + 1) / NB_SAMPLES; final float d_inf = SPLINE_POSITION[index]; final float d_sup = SPLINE_POSITION[index + 1]; velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); distanceCoef = d_inf + (t - t_inf) * velocityCoef; } mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f; mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); // Pin to mMinX <= mCurrX <= mMaxX mCurrX = Math.min(mCurrX, mMaxX); mCurrX = Math.max(mCurrX, mMinX); mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); // Pin to mMinY <= mCurrY <= mMaxY mCurrY = Math.min(mCurrY, mMaxY); mCurrY = Math.max(mCurrY, mMinY); if (mCurrX == mFinalX && mCurrY == mFinalY) { mFinished = true; } break; } } else { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true; } /** * Start scrolling by providing a starting point and the distance to travel. * The scroll will use the default value of 250 milliseconds for the * duration. * * @param startX Starting horizontal scroll offset in pixels. Positive * numbers will scroll the content to the left. * @param startY Starting vertical scroll offset in pixels. Positive numbers * will scroll the content up. * @param dx Horizontal distance to travel. Positive numbers will scroll the * content to the left. * @param dy Vertical distance to travel. Positive numbers will scroll the * content up. */ public void startScroll(int startX, int startY, int dx, int dy) { startScroll(startX, startY, dx, dy, DEFAULT_DURATION); } /** * Start scrolling by providing a starting point, the distance to travel, * and the duration of the scroll. * * @param startX Starting horizontal scroll offset in pixels. Positive * numbers will scroll the content to the left. * @param startY Starting vertical scroll offset in pixels. Positive numbers * will scroll the content up. * @param dx Horizontal distance to travel. Positive numbers will scroll the * content to the left. * @param dy Vertical distance to travel. Positive numbers will scroll the * content up. * @param duration Duration of the scroll in milliseconds. */ public void startScroll(int startX, int startY, int dx, int dy, int duration) { mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = 1.0f / (float) mDuration; } /** * Start scrolling based on a fling gesture. The distance travelled will * depend on the initial velocity of the fling. * * @param startX Starting point of the scroll (X) * @param startY Starting point of the scroll (Y) * @param velocityX Initial velocity of the fling (X) measured in pixels per * second. * @param velocityY Initial velocity of the fling (Y) measured in pixels per * second * @param minX Minimum X value. The scroller will not scroll past this * point. * @param maxX Maximum X value. The scroller will not scroll past this * point. * @param minY Minimum Y value. The scroller will not scroll past this * point. * @param maxY Maximum Y value. The scroller will not scroll past this * point. */ public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) { // Continue a scroll or fling in progress if (mFlywheel && !mFinished) { float oldVel = getCurrVelocity(); float dx = (float) (mFinalX - mStartX); float dy = (float) (mFinalY - mStartY); float hyp = (float) Math.hypot(dx, dy); float ndx = dx / hyp; float ndy = dy / hyp; float oldVelocityX = ndx * oldVel; float oldVelocityY = ndy * oldVel; if (Math.signum(velocityX) == Math.signum(oldVelocityX) && Math.signum(velocityY) == Math.signum(oldVelocityY)) { velocityX += oldVelocityX; velocityY += oldVelocityY; } } mMode = FLING_MODE; mFinished = false; float velocity = (float) Math.hypot(velocityX, velocityY); mVelocity = velocity; mDuration = getSplineFlingDuration(velocity); mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; float coeffX = velocity == 0 ? 1.0f : velocityX / velocity; float coeffY = velocity == 0 ? 1.0f : velocityY / velocity; double totalDistance = getSplineFlingDistance(velocity); mDistance = (int) (totalDistance * Math.signum(velocity)); mMinX = minX; mMaxX = maxX; mMinY = minY; mMaxY = maxY; mFinalX = startX + (int) Math.round(totalDistance * coeffX); // Pin to mMinX <= mFinalX <= mMaxX mFinalX = Math.min(mFinalX, mMaxX); mFinalX = Math.max(mFinalX, mMinX); mFinalY = startY + (int) Math.round(totalDistance * coeffY); // Pin to mMinY <= mFinalY <= mMaxY mFinalY = Math.min(mFinalY, mMaxY); mFinalY = Math.max(mFinalY, mMinY); } private double getSplineDeceleration(float velocity) { return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); } private int getSplineFlingDuration(float velocity) { final double l = getSplineDeceleration(velocity); final double decelMinusOne = DECELERATION_RATE - 1.0; return (int) (1000.0 * Math.exp(l / decelMinusOne)); } private double getSplineFlingDistance(float velocity) { final double l = getSplineDeceleration(velocity); final double decelMinusOne = DECELERATION_RATE - 1.0; return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); } /** * Stops the animation. Contrary to {@link #forceFinished(boolean)}, * aborting the animating cause the scroller to move to the final x and y * position * * @see #forceFinished(boolean) */ public void abortAnimation() { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } /** * Extend the scroll animation. This allows a running animation to scroll * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. * * @param extend Additional time to scroll in milliseconds. * @see #setFinalX(int) * @see #setFinalY(int) */ public void extendDuration(int extend) { int passed = timePassed(); mDuration = passed + extend; mDurationReciprocal = 1.0f / mDuration; mFinished = false; } /** * Returns the time elapsed since the beginning of the scrolling. * * @return The elapsed time in milliseconds. */ public int timePassed() { return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); } /** * Sets the final position (X) for this scroller. * * @param newX The new X offset as an absolute distance from the origin. * @see #extendDuration(int) * @see #setFinalY(int) */ public void setFinalX(int newX) { mFinalX = newX; mDeltaX = mFinalX - mStartX; mFinished = false; } /** * Sets the final position (Y) for this scroller. * * @param newY The new Y offset as an absolute distance from the origin. * @see #extendDuration(int) * @see #setFinalX(int) */ public void setFinalY(int newY) { mFinalY = newY; mDeltaY = mFinalY - mStartY; mFinished = false; } /** * @hide */ public boolean isScrollingInDirection(float xvel, float yvel) { return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && Math.signum(yvel) == Math.signum(mFinalY - mStartY); } static class ViscousFluidInterpolator implements Interpolator { /** Controls the viscous fluid effect (how much of it). */ private static final float VISCOUS_FLUID_SCALE = 8.0f; private static final float VISCOUS_FLUID_NORMALIZE; private static final float VISCOUS_FLUID_OFFSET; static { // must be set to 1.0 (used in viscousFluid()) VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f); // account for very small floating-point error VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f); } private static float viscousFluid(float x) { x *= VISCOUS_FLUID_SCALE; if (x < 1.0f) { x -= (1.0f - (float)Math.exp(-x)); } else { float start = 0.36787944117f; // 1/e == exp(-1) x = 1.0f - (float)Math.exp(1.0f - x); x = start + x * (1.0f - start); } return x; } @Override public float getInterpolation(float input) { final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input); if (interpolated > 0) { return interpolated + VISCOUS_FLUID_OFFSET; } return interpolated; } } } date_time_picker/src/main/java/com/loper7/date_time_picker/utils/StringUtils.kt
New file @@ -0,0 +1,66 @@ package com.loper7.date_time_picker.utils import android.content.Context import android.text.format.DateFormat import java.lang.StringBuilder import java.text.ParseException import java.text.SimpleDateFormat import java.time.* import java.time.format.DateTimeFormatter import java.time.temporal.TemporalField import java.util.* /** * 字符串操作工具包 * */ internal object StringUtils { fun conversionTime( time: String, format: String = "yyyy-MM-dd HH:mm:ss" ): Long { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { val ofPattern = DateTimeFormatter.ofPattern(format) return LocalDateTime.parse(time, ofPattern).toInstant(ZoneOffset.ofHours(8)) .toEpochMilli() } else { val sdf = SimpleDateFormat(format, Locale.getDefault()) try { return sdf.parse(time)?.time?:0 } catch (e: ParseException) { e.printStackTrace() } return 0 } } /** * @param time * @return yy-MM-dd HH:mm格式时间 */ fun conversionTime(time: Long, format: String = "yyyy-MM-dd HH:mm:ss"): String { return DateFormat.format(format, time).toString() } /** * 根据当前日期获得是星期几 * time=yyyy-MM-dd * * @return */ fun getWeek(time: Long): String { val c = Calendar.getInstance() c.timeInMillis = time return when (c[Calendar.DAY_OF_WEEK]) { 1 -> "周日" 2 -> "周一" 3 -> "周二" 4 -> "周三" 5 -> "周四" 6 -> "周五" 7 -> "周六" else -> "" } } } date_time_picker/src/main/java/com/loper7/date_time_picker/utils/lunar/Lunar.kt
New file @@ -0,0 +1,197 @@ package com.loper7.date_time_picker.utils.lunar import android.util.Log import com.loper7.date_time_picker.ext.getMaxDayAtYear import com.loper7.date_time_picker.utils.lunar.LunarConstants.LUNAR_DAY_NAMES import com.loper7.date_time_picker.utils.lunar.LunarConstants.LUNAR_DZ import com.loper7.date_time_picker.utils.lunar.LunarConstants.LUNAR_MONTH_NAMES import com.loper7.date_time_picker.utils.lunar.LunarConstants.LUNAR_TABLE import com.loper7.date_time_picker.utils.lunar.LunarConstants.LUNAR_TG import com.loper7.date_time_picker.utils.lunar.LunarConstants.MIN_LUNAR_YEAR import java.util.* open class Lunar( var year: Int, var month: Int, var isLeapMonth: Boolean, var day: Int, var hour: Int, var minute: Int, var seconds: Int ) { companion object { fun getInstance(timeInMillis: Long): Lunar? { var calendar = Calendar.getInstance() calendar.timeInMillis = timeInMillis return getInstance(calendar) } fun getInstance(calendar: Calendar = Calendar.getInstance()): Lunar? { //传入的时间超出了计算范围 if (!hasLunarInfo(calendar)) return null var lunarYear: Int = calendar[Calendar.YEAR] var lunarMonth = 0 var lunarDay = 0 val lunarHour = calendar[Calendar.HOUR_OF_DAY] val lunarLeapMonth: Int var isLeap = false var doffset = calendar[Calendar.DAY_OF_YEAR] - 1 var hexvalue = LUNAR_TABLE[lunarYear - MIN_LUNAR_YEAR] //农历正月的偏移 var loffset = hexvalue and 0xFF //如果当前离1月1号的天数比正月离元月的天数还小那么则应该是上一个农历年 if (loffset > doffset) { lunarYear-- doffset += GregorianCalendar().getMaxDayAtYear(lunarYear) hexvalue = LUNAR_TABLE[lunarYear - MIN_LUNAR_YEAR] loffset = hexvalue and 0xFF } var days = doffset - loffset + 1 //农历闰月 lunarLeapMonth = hexvalue shr 8 and 0xF val len = if (lunarLeapMonth > 0) 13 else 12 //开始循环取 var v = 0 var cd = 0 for (i in 0 until len) { v = if (lunarLeapMonth in 1..i) { if (i == lunarLeapMonth) { hexvalue shr 12 and 0x1 } else { hexvalue shr 24 - i + 1 and 0x1 } } else { hexvalue shr 24 - i and 0x1 } cd = 29 + v days -= cd if (days <= 0) { lunarDay = days + cd lunarMonth = i + 1 if (lunarLeapMonth in 1..i) { isLeap = i == lunarLeapMonth --lunarMonth } break } } return Lunar( lunarYear, lunarMonth, isLeap, lunarDay, lunarHour, calendar[Calendar.MINUTE], calendar[Calendar.SECOND] ) } /** * 是否有农历信息 * * @param calendar * @return */ fun hasLunarInfo(calendar: Calendar): Boolean { return try { val syear = calendar[Calendar.YEAR] val dayoffset = calendar[Calendar.DAY_OF_YEAR] - 1 val lindex = syear - MIN_LUNAR_YEAR if (lindex < 0 || lindex >= LUNAR_TABLE.size) { return false } var lyear = syear val hexValue = LUNAR_TABLE[lindex] val ldayoffset = hexValue and 0xFF if (ldayoffset > dayoffset) { lyear-- } lyear >= MIN_LUNAR_YEAR } catch (e: Throwable) { e.printStackTrace() false } } } /** * 获取农历干支纪年 * * @return */ val yearName: String get() { var tg = LUNAR_TG[(year - 4) % 10] var dz = LUNAR_DZ[(year - 4) % 12] return "${tg}${dz}年" } /** * 获取农历月名称 * * @return */ val monthName: String get() = (if (isLeapMonth) "闰" else "") + LUNAR_MONTH_NAMES[month - 1] /** * 获取农历日名称 * * @return */ val dayName: String get() = LUNAR_DAY_NAMES[day - 1] /** * 获取农历时辰名称 */ val hourName: String get() = "${LUNAR_DZ[(hour + 1) / 2 % 12]}时" /** * 获取农历年月中最多的天数 */ fun getMaxDayInMonth(): Int { val index: Int = year - MIN_LUNAR_YEAR val hexValue: Int = LUNAR_TABLE[index] return if (isLeapMonth) { (hexValue shr 12 and 0x1) + 29 } else (hexValue shr 24 - month + 1 and 0x1) + 29 } override fun equals(o: Any?): Boolean { if (o == null || o !is Lunar) return false return o.year == year && o.month == month && o.isLeapMonth == isLeapMonth } override fun toString(): String { var map = mutableMapOf<String, Any>() map["year"] = year map["month"] = month map["day"] = day map["hour"] = hour map["minute"] = minute map["seconds"] = seconds map["isLeapMonth"] = isLeapMonth map["yearName"] = yearName map["monthName"] = monthName map["dayName"] = dayName map["hourName"] = hourName return map.toString() } } date_time_picker/src/main/java/com/loper7/date_time_picker/utils/lunar/LunarConstants.kt
New file @@ -0,0 +1,72 @@ package com.loper7.date_time_picker.utils.lunar /** *@Author loper7 *@Date 2021/12/2 10:56 *@Description **/ object LunarConstants { /** 农历表中最小的年份 **/ const val MIN_LUNAR_YEAR = 1899 /** 未找到农历信息 **/ const val NOT_FOUND_LUNAR = -1 /** 农历月名称 **/ val LUNAR_MONTH_NAMES = arrayOf( "正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "冬月", "腊月" ) /** 农历日名称 **/ val LUNAR_DAY_NAMES = arrayOf( "初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十", "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十", "廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十" ) /** 天干**/ val LUNAR_TG = arrayOf("甲", "乙", "丙", "丁", "戊", "己","庚","辛","壬","癸") /** 地支**/ val LUNAR_DZ = arrayOf("子", "丑", "寅", "卯", "辰", "巳", "午", "未","申", "酉", "戌", "亥") /** * 农历信息表 * * 1899~2135年 农历信息 * * eg:2012年 -> 0x1754416 -> (0001 0111 0101 0100 0100 0001 0110)2 * 从后往前读8位 表示 正月初一 距离 公历1月1日 的天数: (0001 0110)2 -> 22 天 * 继续往前读4位 表示 闰哪个月 (0100)2 -> 4 即 闰四月 (0表示该年没有闰月) * 继续往前读13位 表示 每月天数信息 其中前12位表示正月到腊月的天数信息 第13位表示闰月的天数信息 (1 0111 0101 0100)2 -> 正月大、二月小、三月大 。。。腊月小、闰四月小 * * 注:农历月大30天 月小29天 */ val LUNAR_TABLE = intArrayOf( 0x156A028, 0x97A81E, 0x95C031, 0x14AE026, 0xA9A51C, 0x1A4C02E, 0x1B2A022, 0xCAB418, 0xAD402B, 0x135A020, //1899-1908 0xABA215, 0x95C028, 0x14B661D, 0x149A030, 0x1A4A024, 0x1A4B519, 0x16A802C, 0x1AD4021, 0x15B4216, 0x12B6029, //1909-1918 0x92F71F, 0x92E032, 0x1496026, 0x169651B, 0xD4A02E, 0xDA8023, 0x156B417, 0x56C02B, 0x12AE020, 0xA5E216, //1919-1928 0x92E028, 0xCAC61D, 0x1A9402F, 0x1D4A024, 0xD53519, 0xB5A02C, 0x56C022, 0x10DD317, 0x125C029, 0x191B71E, //1929-1938 0x192A031, 0x1A94026, 0x1B1561A, 0x16AA02D, 0xAD4023, 0x14B7418, 0x4BA02B, 0x125A020, 0x1A56215, 0x152A028, //1939-1948 0x16AA71C, 0xD9402F, 0x16AA024, 0xA6B51A, 0x9B402C, 0x14B6021, 0x8AF317, 0xA5602A, 0x153481E, 0x1D2A030, //1949-1958 0xD54026, 0x15D461B, 0x156A02D, 0x96C023, 0x155C418, 0x14AE02B, 0xA4C020, 0x1E4C314, 0x1B2A027, 0xB6A71D, //1959-1968 0xAD402F, 0x12DA024, 0x9BA51A, 0x95A02D, 0x149A021, 0x1A9A416, 0x1A4A029, 0x1AAA81E, 0x16A8030, 0x16D4025, //1969-1978 0x12B561B, 0x12B602E, 0x936023, 0x152E418, 0x149602B, 0x164EA20, 0xD4A032, 0xDA8027, 0x15E861C, 0x156C02F, //1979-1988 0x12AE024, 0x95E51A, 0x92E02D, 0xC96022, 0xE94316, 0x1D4A028, 0xD6A81E, 0xB58031, 0x156C025, 0x12DA51B, //1989-1998 0x125C02E, 0x192C023, 0x1B2A417, 0x1A9402A, 0x1B4A01F, 0xEAA215, 0xAD4027, 0x157671C, 0x4BA030, 0x125A025, //1999-2008 0x1956519, 0x152A02C, 0x1694021, 0x1754416, 0x15AA028, 0xABA91E, 0x974031, 0x14B6026, 0xA2F61B, 0xA5602E, //2009-2018 0x1526023, 0xF2A418, 0xD5402A, 0x15AA01F, 0xB6A215, 0x96C028, 0x14DC61C, 0x149C02F, 0x1A4C024, 0x1D4C519, //2019-2028 0x1AA602B, 0xB54021, 0xED4316, 0x12DA029, 0x95EB1E, 0x95A031, 0x149A026, 0x1A1761B, 0x1A4A02D, 0x1AA4022, //2029-2038 0x1BA8517, 0x16B402A, 0xADA01F, 0xAB6215, 0x936028, 0x14AE71D, 0x149602F, 0x154A024, 0x164B519, 0xDA402C, //2039-2048 0x15B4020, 0x96D316, 0x126E029, 0x93E81F, 0x92E031, 0xC96026, 0xD1561B, 0x1D4A02D, 0xD64022, 0x14D9417, //2049-2058 0x155C02A, 0x125C020, 0x1A5C314, 0x192C027, 0x1AAA71C, 0x1A9402F, 0x1B4A023, 0xBAA519, 0xAD402C, 0x14DA021, //2059-2068 0xABA416, 0xA5A029, 0x153681E, 0x152A031, 0x1694025, 0x16D461A, 0x15AA02D, 0xAB4023, 0x1574417, 0x14B602A, //2069-2078 0xA56020, 0x164E315, 0xD26027, 0xE6671C, 0xD5402F, 0x15AA024, 0x96B519, 0x96C02C, 0x14AE021, 0xA9C417, //2079-2088 0x1A4C028, 0x1D2C81D, 0x1AA4030, 0x1B54025, 0xD5561A, 0xADA02D, 0x95C023, 0x153A418, 0x149A02A, 0x1A2A01F, //2089-2098 0x1E4A214, 0x1AA4027, 0x1B6471C, 0x16B402F, 0xABA025, 0x9B651B, 0x93602D, 0x1496022, 0x1A96417, 0x154A02A, //2099-2108 0x16AA91E, 0xDA4031, 0x15AC026, 0xAEC61C, 0x126E02E, 0x92E024, 0xD2E419, 0xA9602C, 0xD4A020, 0xF4A315, //2109-2118 0xD54028, 0x155571D, 0x155A02F, 0xA5C025, 0x195C51A, 0x152C02D, 0x1A94021, 0x1C95416, 0x1B2A029, 0xB5A91F, //2119-2128 0xAD4031, 0x14DA026, 0xA3B61C, 0xA5A02F, 0x151A023, 0x1A2B518, 0x165402B //2129-2135 ) } date_time_picker/src/main/res/drawable/shape_bg_oval_accent.xml
New file @@ -0,0 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="@color/colorAccent"/> </shape> date_time_picker/src/main/res/drawable/shape_bg_round_white_5.xml
New file @@ -0,0 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <corners android:radius="5dp"/> <solid android:color="@color/colorTextWhite"/> </shape> date_time_picker/src/main/res/drawable/shape_bg_top_round_white_15.xml
New file @@ -0,0 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <corners android:topLeftRadius="15dp" android:topRightRadius="15dp"/> <solid android:color="@color/colorTextWhite"/> </shape> date_time_picker/src/main/res/layout/dt_dialog_time_picker.xml
New file @@ -0,0 +1,131 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical"> <LinearLayout android:id="@+id/linear_bg" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:paddingTop="25dp" android:background="@drawable/shape_bg_round_white_5" android:orientation="vertical"> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:visibility="gone" android:layout_marginBottom="20dp" android:textStyle="bold" android:textColor="@color/colorTextBlack" android:textSize="18sp" /> <TextView android:id="@+id/tv_choose_date" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="12dp" android:layout_gravity="center_horizontal" android:text="" android:textColor="@color/colorTextBlack" android:textSize="14sp" /> <View android:id="@+id/divider_top" android:layout_width="match_parent" android:layout_height="0.6dp" android:background="#E5E5E5" /> <com.loper7.date_time_picker.DateTimePicker android:id="@+id/dateTimePicker" android:layout_width="match_parent" android:layout_height="wrap_content" /> <LinearLayout android:id="@+id/linear_now" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:orientation="horizontal"> <TextView android:id="@+id/tv_go_back" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="120dp" android:text="回到今天" android:textColor="@color/colorTextGrayDark" android:textSize="14sp" /> <TextView android:id="@+id/btn_today" android:layout_width="55dp" android:layout_height="55dp" android:layout_marginTop="25dp" android:layout_marginBottom="30dp" android:background="@drawable/shape_bg_oval_accent" android:elevation="2dp" android:gravity="center" android:text="今" android:textColor="@color/colorTextWhite" android:textSize="26dp" /> </LinearLayout> <View android:id="@+id/divider_bottom" android:layout_width="match_parent" android:layout_height="0.6dp" android:layout_marginTop="10dp" android:background="#E5E5E5" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:id="@+id/dialog_cancel" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:background="?android:attr/selectableItemBackground" android:clickable="true" android:textStyle="bold" android:gravity="center_horizontal" android:padding="16dp" android:text="取消" android:textColor="@color/colorTextGray" android:textSize="16sp" android:visibility="visible" /> <View android:id="@+id/dialog_select_border" android:layout_width="0.6dp" android:layout_height="match_parent" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:background="#E5E5E5" /> <TextView android:id="@+id/dialog_submit" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:background="?android:attr/selectableItemBackground" android:clickable="true" android:gravity="center_horizontal" android:padding="16dp" android:text="确定" android:textStyle="bold" android:textColor="@color/colorAccent" android:textSize="16sp" /> </LinearLayout> </LinearLayout> </LinearLayout> date_time_picker/src/main/res/layout/dt_dialog_week_picker.xml
New file @@ -0,0 +1,90 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical"> <LinearLayout android:id="@+id/linear_bg" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:paddingTop="25dp" android:background="@drawable/shape_bg_round_white_5" android:orientation="vertical"> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:visibility="gone" android:layout_marginBottom="25dp" android:textStyle="bold" android:textColor="@color/colorTextBlack" android:textSize="18sp" /> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_week" android:layout_width="match_parent" android:layout_height="wrap_content" android:tag="np_datetime_year" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_selectedTextColor="@color/colorPrimary" app:np_height="184dp" app:np_wheelItemCount="3" /> <View android:id="@+id/divider_bottom" android:layout_width="match_parent" android:layout_height="0.6dp" android:layout_marginTop="10dp" android:background="#E5E5E5" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:id="@+id/dialog_cancel" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:background="?android:attr/selectableItemBackground" android:clickable="true" android:textStyle="bold" android:gravity="center_horizontal" android:padding="16dp" android:text="取消" android:textColor="@color/colorTextGray" android:textSize="16sp" android:visibility="visible" /> <View android:id="@+id/dialog_select_border" android:layout_width="0.6dp" android:layout_height="match_parent" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:background="#E5E5E5" /> <TextView android:id="@+id/dialog_submit" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:background="?android:attr/selectableItemBackground" android:clickable="true" android:gravity="center_horizontal" android:padding="16dp" android:text="确定" android:textStyle="bold" android:textColor="@color/colorAccent" android:textSize="16sp" /> </LinearLayout> </LinearLayout> </LinearLayout> date_time_picker/src/main/res/layout/dt_layout_date_picker.xml
New file @@ -0,0 +1,80 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="center" android:gravity="center" android:orientation="horizontal"> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_datetime_year" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:tag="np_datetime_year" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_fadingEdgeEnabled="false" app:np_height="184dp" app:np_wheelItemCount="3" /> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_datetime_month" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_fadingEdgeEnabled="false" app:np_height="184dp" app:np_wheelItemCount="3" /> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_datetime_day" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_fadingEdgeEnabled="false" app:np_height="184dp" app:np_wheelItemCount="3" /> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_datetime_hour" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_fadingEdgeEnabled="false" app:np_height="184dp" app:np_wheelItemCount="3" /> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_datetime_minute" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_fadingEdgeEnabled="false" app:np_height="184dp" app:np_wheelItemCount="3" /> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_datetime_second" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_fadingEdgeEnabled="false" app:np_height="184dp" app:np_wheelItemCount="3" /> </LinearLayout> date_time_picker/src/main/res/layout/dt_layout_date_picker_globalization.xml
New file @@ -0,0 +1,74 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="center" android:gravity="center" android:orientation="horizontal"> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_datetime_day" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_height="184dp" app:np_wheelItemCount="3" /> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_datetime_month" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_height="184dp" app:np_wheelItemCount="3" /> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_datetime_year" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:tag="np_datetime_year" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_height="184dp" app:np_wheelItemCount="3" /> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_datetime_hour" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_height="184dp" app:np_wheelItemCount="3" /> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_datetime_minute" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_height="184dp" app:np_wheelItemCount="3" /> <com.loper7.date_time_picker.number_picker.NumberPicker android:id="@+id/np_datetime_second" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" app:np_dividerColor="#E5E5E5" app:np_dividerThickness="0.6dp" app:np_height="184dp" app:np_wheelItemCount="3" /> </LinearLayout> date_time_picker/src/main/res/values/attrs.xml
New file @@ -0,0 +1,74 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <attr name="numberPickerStyle" format="reference" /> <declare-styleable name="NumberPicker"> <attr name="np_width" format="dimension" /> <attr name="np_height" format="dimension" /> <attr name="np_accessibilityDescriptionEnabled" format="boolean" /> <attr name="np_divider" format="reference" /> <attr name="np_dividerType" format="enum"> <enum name="side_lines" value="0" /> <enum name="underline" value="1" /> </attr> <attr name="np_dividerColor" format="color" /> <attr name="np_dividerDistance" format="dimension" /> <attr name="np_dividerLength" format="dimension" /> <attr name="np_dividerThickness" format="dimension" /> <attr name="np_fadingEdgeEnabled" format="boolean" /> <attr name="np_fadingEdgeStrength" format="float" /> <attr name="np_formatter" format="string" /> <attr name="np_hideWheelUntilFocused" format="boolean" /> <attr name="np_itemSpacing" format="dimension" /> <attr name="np_lineSpacingMultiplier" format="float" /> <attr name="np_max" format="integer" /> <attr name="np_maxFlingVelocityCoefficient" format="integer" /> <attr name="np_min" format="integer" /> <attr name="np_order" format="enum"> <enum name="ascending" value="0" /> <enum name="descending" value="1" /> </attr> <attr name="np_orientation" format="enum"> <enum name="horizontal" value="0" /> <enum name="vertical" value="1" /> </attr> <attr name="np_scrollerEnabled" format="boolean" /> <attr name="np_selectedTextAlign" format="enum"> <enum name="selectedTextAlignRight" value="0" /> <enum name="selectedTextAlignCenter" value="1" /> <enum name="selectedTextAlignLeft" value="2" /> </attr> <attr name="np_selectedTextColor" format="color" /> <attr name="np_selectedTextSize" format="dimension" /> <attr name="np_selectedTextStrikeThru" format="boolean" /> <attr name="np_selectedTextUnderline" format="boolean" /> <attr name="np_selectedTypeface" format="string" /> <attr name="np_textAlign" format="enum"> <enum name="textAlignRight" value="0" /> <enum name="textAlignCenter" value="1" /> <enum name="textAlignLeft" value="2" /> </attr> <attr name="np_textColor" format="color" /> <attr name="np_textSize" format="dimension" /> <attr name="np_textStrikeThru" format="boolean" /> <attr name="np_textUnderline" format="boolean" /> <attr name="np_typeface" format="string" /> <attr name="np_value" format="integer" /> <attr name="np_wheelItemCount" format="integer" /> <attr name="np_wrapSelectorWheel" format="boolean" /> <attr name="np_textBold" format="boolean" /> <attr name="np_selectedTextBold" format="boolean" /> </declare-styleable> <declare-styleable name="DateTimePicker"> <attr name="dt_showLabel" format="boolean" /> <attr name="dt_textColor" format="color" /> <attr name="dt_dividerColor" format="color" /> <attr name="dt_themeColor" format="color" /> <attr name="dt_selectTextSize" format="dimension" /> <attr name="dt_normalTextSize" format="dimension" /> <attr name="dt_layout" format="reference" /> <attr name="dt_textBold" format="boolean" /> <attr name="dt_selectedTextBold" format="boolean" /> </declare-styleable> </resources> date_time_picker/src/main/res/values/colors.xml
New file @@ -0,0 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#000000</color> <color name="colorPrimaryDark">#000000</color> <color name="colorAccent">#000000</color> <color name="colorAccentLight">#000000</color> <color name="colorAccentDark">#000000</color> <!--字体颜色--> <color name="colorTextHint">#B2B2B2</color> <color name="colorTextBlack">#333333</color> <color name="colorTextBlackLight">#4d4d4d</color> <color name="colorTextGrayLight">#C3C3C3</color> <color name="colorTextGrayDark">#666666</color> <color name="colorTextGray">#999999</color> <color name="colorTextWhite">#ffffff</color> <color name="colorDivider">#E5E5E5</color> </resources> date_time_picker/src/main/res/values/ids.xml
New file @@ -0,0 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <!-- Just adding these so I wont have to remove a lot of code from NumberPicker.java. --> <item name="np__increment" type="id" /> <item name="np__decrement" type="id" /> <item name="design_bottom_sheet" type="id" /> </resources> date_time_picker/src/main/res/values/styles.xml
New file @@ -0,0 +1,13 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <style name="DateTimePicker_BottomSheetDialog" parent="Theme.Design.Light.BottomSheetDialog"> <item name="android:backgroundDimEnabled">true</item> </style> <style name="Theme.Design.Light.BottomSheetDialog" parent="Theme.AppCompat.Light.Dialog"> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowAnimationStyle">@style/Animation.Design.BottomSheetDialog</item> <item name="bottomSheetStyle">@style/Widget.Design.BottomSheet.Modal</item> </style> </resources> date_time_picker/src/test/java/com/loper7/date_time_picker/ExampleUnitTest.kt
New file @@ -0,0 +1,17 @@ package com.loper7.date_time_picker import org.junit.Test import org.junit.Assert.* /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) } } settings.gradle
@@ -2,3 +2,4 @@ include ':app' include ':expand_button' include ':library' include ':date_time_picker'