管灌系统巡查员智能手机App
zuoxiao
2025-01-23 b6f46408cb3dc8b01051953e5c68de6c9195db60
1.修复分水房点击屏幕不变蓝bug。
2.处理工单添加处理时间弹窗。
33个文件已添加
6个文件已修改
6890 ■■■■■ 已修改文件
app/build.gradle 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/assets/js/map.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/activity/OrderDealActivity.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/dayu/pipirrapp/activity/OrderDetailActivity.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_order_deal.xml 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/.gitignore 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/build.gradle 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/consumer-rules.pro 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/proguard-rules.pro 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/androidTest/java/com/loper7/date_time_picker/ExampleInstrumentedTest.kt 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/AndroidManifest.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/DateTimeConfig.kt 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/DateTimePicker.kt 443 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/controller/BaseDateTimeController.kt 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/controller/DateTimeController.kt 320 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/controller/DateTimeInterface.kt 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/dialog/CardDatePickerDialog.kt 633 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/dialog/CardWeekPickerDialog.kt 379 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/ext/CalendarExt.kt 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/ext/ContextExt.kt 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/ext/ListExt.kt 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/number_picker/NumberPicker.java 3051 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/number_picker/Scroller.java 596 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/utils/StringUtils.kt 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/utils/lunar/Lunar.kt 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/java/com/loper7/date_time_picker/utils/lunar/LunarConstants.kt 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/res/drawable/shape_bg_oval_accent.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/res/drawable/shape_bg_round_white_5.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/res/drawable/shape_bg_top_round_white_15.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/res/layout/dt_dialog_time_picker.xml 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/res/layout/dt_dialog_week_picker.xml 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/res/layout/dt_layout_date_picker.xml 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/res/layout/dt_layout_date_picker_globalization.xml 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/res/values/attrs.xml 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/res/values/colors.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/res/values/ids.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/main/res/values/styles.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
date_time_picker/src/test/java/com/loper7/date_time_picker/ExampleUnitTest.kt 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
settings.gradle 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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&mdash;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'