From b6f46408cb3dc8b01051953e5c68de6c9195db60 Mon Sep 17 00:00:00 2001 From: zuoxiao <470321431@qq.com> Date: 星期四, 23 一月 2025 10:15:46 +0800 Subject: [PATCH] 1.修复分水房点击屏幕不变蓝bug。 2.处理工单添加处理时间弹窗。 --- date_time_picker/src/main/res/values/ids.xml | 9 app/src/main/assets/js/map.js | 4 date_time_picker/src/main/java/com/loper7/date_time_picker/controller/DateTimeInterface.kt | 51 app/src/main/java/com/dayu/pipirrapp/activity/OrderDealActivity.java | 24 date_time_picker/src/main/java/com/loper7/date_time_picker/ext/ListExt.kt | 49 app/src/main/res/layout/activity_order_deal.xml | 41 date_time_picker/src/main/java/com/loper7/date_time_picker/DateTimePicker.kt | 443 +++ date_time_picker/src/main/res/layout/dt_dialog_time_picker.xml | 131 + date_time_picker/src/main/res/drawable/shape_bg_oval_accent.xml | 5 date_time_picker/src/main/res/layout/dt_layout_date_picker_globalization.xml | 74 date_time_picker/src/test/java/com/loper7/date_time_picker/ExampleUnitTest.kt | 17 date_time_picker/proguard-rules.pro | 21 date_time_picker/src/main/java/com/loper7/date_time_picker/ext/ContextExt.kt | 26 date_time_picker/build.gradle | 38 date_time_picker/src/main/res/values/colors.xml | 20 date_time_picker/consumer-rules.pro | 0 date_time_picker/src/androidTest/java/com/loper7/date_time_picker/ExampleInstrumentedTest.kt | 24 date_time_picker/src/main/java/com/loper7/date_time_picker/number_picker/NumberPicker.java | 3051 ++++++++++++++++++++++++++ app/build.gradle | 1 app/src/main/java/com/dayu/pipirrapp/activity/OrderDetailActivity.java | 9 date_time_picker/src/main/java/com/loper7/date_time_picker/dialog/CardWeekPickerDialog.kt | 379 +++ date_time_picker/src/main/java/com/loper7/date_time_picker/DateTimeConfig.kt | 75 settings.gradle | 1 date_time_picker/.gitignore | 1 date_time_picker/src/main/res/layout/dt_layout_date_picker.xml | 80 date_time_picker/src/main/AndroidManifest.xml | 2 date_time_picker/src/main/res/values/styles.xml | 13 date_time_picker/src/main/java/com/loper7/date_time_picker/ext/CalendarExt.kt | 200 + date_time_picker/src/main/java/com/loper7/date_time_picker/number_picker/Scroller.java | 596 +++++ date_time_picker/src/main/java/com/loper7/date_time_picker/controller/BaseDateTimeController.kt | 43 date_time_picker/src/main/java/com/loper7/date_time_picker/dialog/CardDatePickerDialog.kt | 633 +++++ date_time_picker/src/main/res/values/attrs.xml | 74 date_time_picker/src/main/java/com/loper7/date_time_picker/utils/lunar/LunarConstants.kt | 72 date_time_picker/src/main/java/com/loper7/date_time_picker/controller/DateTimeController.kt | 320 ++ date_time_picker/src/main/java/com/loper7/date_time_picker/utils/lunar/Lunar.kt | 197 + date_time_picker/src/main/res/drawable/shape_bg_top_round_white_15.xml | 5 date_time_picker/src/main/res/layout/dt_dialog_week_picker.xml | 90 date_time_picker/src/main/java/com/loper7/date_time_picker/utils/StringUtils.kt | 66 date_time_picker/src/main/res/drawable/shape_bg_round_white_5.xml | 5 39 files changed, 6,888 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fd2619d..2211ad5 100644 --- a/app/build.gradle +++ b/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' diff --git a/app/src/main/assets/js/map.js b/app/src/main/assets/js/map.js index ba614dd..85b3113 100644 --- a/app/src/main/assets/js/map.js +++ b/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; diff --git a/app/src/main/java/com/dayu/pipirrapp/activity/OrderDealActivity.java b/app/src/main/java/com/dayu/pipirrapp/activity/OrderDealActivity.java index c33315d..0d707c0 100644 --- a/app/src/main/java/com/dayu/pipirrapp/activity/OrderDealActivity.java +++ b/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); diff --git a/app/src/main/java/com/dayu/pipirrapp/activity/OrderDetailActivity.java b/app/src/main/java/com/dayu/pipirrapp/activity/OrderDetailActivity.java index 2a1fd15..df93212 100644 --- a/app/src/main/java/com/dayu/pipirrapp/activity/OrderDetailActivity.java +++ b/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); } diff --git a/app/src/main/res/layout/activity_order_deal.xml b/app/src/main/res/layout/activity_order_deal.xml index 3eab21e..3bc03cb 100644 --- a/app/src/main/res/layout/activity_order_deal.xml +++ b/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"> diff --git a/date_time_picker/.gitignore b/date_time_picker/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/date_time_picker/.gitignore @@ -0,0 +1 @@ +/build diff --git a/date_time_picker/build.gradle b/date_time_picker/build.gradle new file mode 100644 index 0000000..eae0483 --- /dev/null +++ b/date_time_picker/build.gradle @@ -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' + +} diff --git a/date_time_picker/consumer-rules.pro b/date_time_picker/consumer-rules.pro new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/date_time_picker/consumer-rules.pro diff --git a/date_time_picker/proguard-rules.pro b/date_time_picker/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/date_time_picker/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/date_time_picker/src/androidTest/java/com/loper7/date_time_picker/ExampleInstrumentedTest.kt b/date_time_picker/src/androidTest/java/com/loper7/date_time_picker/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..5411ceb --- /dev/null +++ b/date_time_picker/src/androidTest/java/com/loper7/date_time_picker/ExampleInstrumentedTest.kt @@ -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) + } +} diff --git a/date_time_picker/src/main/AndroidManifest.xml b/date_time_picker/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6fc1eaf --- /dev/null +++ b/date_time_picker/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.loper7.date_time_picker" /> diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/DateTimeConfig.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/DateTimeConfig.kt new file mode 100644 index 0000000..a9a0f57 --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/DateTimeConfig.kt @@ -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()) + } + + +} \ No newline at end of file diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/DateTimePicker.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/DateTimePicker.kt new file mode 100644 index 0000000..14a5a9e --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/DateTimePicker.kt @@ -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() + } + + /** + * 璁剧疆鑷畾涔塴ayout + */ + 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绫诲瀷锛圖ateTimeConfig-> 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) + } + + /** + * 鑾峰彇绫诲瀷瀵瑰簲鐨凬umberPicker + * @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 + } + + +} \ No newline at end of file diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/controller/BaseDateTimeController.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/controller/BaseDateTimeController.kt new file mode 100644 index 0000000..0b25fcc --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/controller/BaseDateTimeController.kt @@ -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 + } + + } + +} \ No newline at end of file diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/controller/DateTimeController.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/controller/DateTimeController.kt new file mode 100644 index 0000000..fdda9c7 --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/controller/DateTimeController.kt @@ -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 + } +} diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/controller/DateTimeInterface.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/controller/DateTimeInterface.kt new file mode 100644 index 0000000..f2366de --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/controller/DateTimeInterface.kt @@ -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绫诲瀷锛圖ateTimeConfig-> 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 +} \ No newline at end of file diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/dialog/CardDatePickerDialog.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/dialog/CardDatePickerDialog.kt new file mode 100644 index 0000000..61b5a15 --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/dialog/CardDatePickerDialog.kt @@ -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 + + //璁剧疆鑷畾涔塴ayout + 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骞碝M鏈坉d鏃� ") + 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 + } + + /** + * 璁剧疆鑷畾涔夐�夋嫨鍣╨ayout + * @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() + } + +} \ No newline at end of file diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/dialog/CardWeekPickerDialog.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/dialog/CardWeekPickerDialog.kt new file mode 100644 index 0000000..ca58ec8 --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/dialog/CardWeekPickerDialog.kt @@ -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 鍛╢ormat瀛楃涓� + * @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) + } + } + +} \ No newline at end of file diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/ext/CalendarExt.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/ext/CalendarExt.kt new file mode 100644 index 0000000..a407b25 --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/ext/CalendarExt.kt @@ -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) +} + diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/ext/ContextExt.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/ext/ContextExt.kt new file mode 100644 index 0000000..e0727e6 --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/ext/ContextExt.kt @@ -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() +} diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/ext/ListExt.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/ext/ListExt.kt new file mode 100644 index 0000000..d281b14 --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/ext/ListExt.kt @@ -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 +} \ No newline at end of file diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/number_picker/NumberPicker.java b/date_time_picker/src/main/java/com/loper7/date_time_picker/number_picker/NumberPicker.java new file mode 100644 index 0000000..32642a4 --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/number_picker/NumberPicker.java @@ -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; + } +} diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/number_picker/Scroller.java b/date_time_picker/src/main/java/com/loper7/date_time_picker/number_picker/Scroller.java new file mode 100644 index 0000000..b94a846 --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/number_picker/Scroller.java @@ -0,0 +1,596 @@ +package com.loper7.date_time_picker.number_picker; +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context; +import android.hardware.SensorManager; +import android.os.Build; +import android.view.ViewConfiguration; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + + +/** + * <p>This class encapsulates scrolling. You can use scrollers ({@link Scroller} + * to collect the data you need to produce a scrolling animation—for + * example, in response to a fling gesture. Scrollers track scroll offsets + * for you over time, but they don't automatically apply those positions + * to your view. It's your responsibility to get and apply new coordinates + * at a rate that will make the scrolling animation look smooth.</p> + * + * <p>Here is a simple example:</p> + * + * <pre> private Scroller mScroller = new Scroller(context); + * ... + * public void zoomIn() { + * // Revert any animation currently in progress + * mScroller.forceFinished(true); + * // Start scrolling by providing a starting point and + * // the distance to travel + * mScroller.startScroll(0, 0, 100, 0); + * // Invalidate to request a redraw + * invalidate(); + * }</pre> + * + * <p>To track the changing positions of the x/y coordinates, use + * {@link #computeScrollOffset}. The method returns a boolean to indicate + * whether the scroller is finished. If it isn't, it means that a fling or + * programmatic pan operation is still in progress. You can use this method to + * find the current offsets of the x and y coordinates, for example:</p> + * + * <pre>if (mScroller.computeScrollOffset()) { + * // Get current x and y positions + * int currX = mScroller.getCurrX(); + * int currY = mScroller.getCurrY(); + * ... + * }</pre> + */ +public class Scroller { + private final Interpolator mInterpolator; + + private int mMode; + + private int mStartX; + private int mStartY; + private int mFinalX; + private int mFinalY; + + private int mMinX; + private int mMaxX; + private int mMinY; + private int mMaxY; + + private int mCurrX; + private int mCurrY; + private long mStartTime; + private int mDuration; + private float mDurationReciprocal; + private float mDeltaX; + private float mDeltaY; + private boolean mFinished; + private boolean mFlywheel; + + private float mVelocity; + private float mCurrVelocity; + private int mDistance; + + private float mFlingFriction = ViewConfiguration.getScrollFriction(); + + private static final int DEFAULT_DURATION = 250; + private static final int SCROLL_MODE = 0; + private static final int FLING_MODE = 1; + + private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); + private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) + private static final float START_TENSION = 0.5f; + private static final float END_TENSION = 1.0f; + private static final float P1 = START_TENSION * INFLEXION; + private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION); + + private static final int NB_SAMPLES = 100; + private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; + private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; + + private float mDeceleration; + private final float mPpi; + + // A context-specific coefficient adjusted to physical values. + private float mPhysicalCoeff; + + static { + float x_min = 0.0f; + float y_min = 0.0f; + for (int i = 0; i < NB_SAMPLES; i++) { + final float alpha = (float) i / NB_SAMPLES; + + float x_max = 1.0f; + float x, tx, coef; + while (true) { + x = x_min + (x_max - x_min) / 2.0f; + coef = 3.0f * x * (1.0f - x); + tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x; + if (Math.abs(tx - alpha) < 1E-5) break; + if (tx > alpha) x_max = x; + else x_min = x; + } + SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x; + + float y_max = 1.0f; + float y, dy; + while (true) { + y = y_min + (y_max - y_min) / 2.0f; + coef = 3.0f * y * (1.0f - y); + dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y; + if (Math.abs(dy - alpha) < 1E-5) break; + if (dy > alpha) y_max = y; + else y_min = y; + } + SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; + } + SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; + } + + /** + * Create a Scroller with the default duration and interpolator. + */ + public Scroller(Context context) { + this(context, null); + } + + /** + * Create a Scroller with the specified interpolator. If the interpolator is + * null, the default (viscous) interpolator will be used. "Flywheel" behavior will + * be in effect for apps targeting Honeycomb or newer. + */ + public Scroller(Context context, Interpolator interpolator) { + this(context, interpolator, + context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB); + } + + /** + * Create a Scroller with the specified interpolator. If the interpolator is + * null, the default (viscous) interpolator will be used. Specify whether or + * not to support progressive "flywheel" behavior in flinging. + */ + public Scroller(Context context, Interpolator interpolator, boolean flywheel) { + mFinished = true; + if (interpolator == null) { + mInterpolator = new ViscousFluidInterpolator(); + } else { + mInterpolator = interpolator; + } + mPpi = context.getResources().getDisplayMetrics().density * 160.0f; + mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); + mFlywheel = flywheel; + + mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning + } + + /** + * The amount of friction applied to flings. The default value + * is {@link ViewConfiguration#getScrollFriction}. + * + * @param friction A scalar dimension-less value representing the coefficient of + * friction. + */ + public final void setFriction(float friction) { + mDeceleration = computeDeceleration(friction); + mFlingFriction = friction; + } + + private float computeDeceleration(float friction) { + return SensorManager.GRAVITY_EARTH // g (m/s^2) + * 39.37f // inch/meter + * mPpi // pixels per inch + * friction; + } + + /** + * + * Returns whether the scroller has finished scrolling. + * + * @return True if the scroller has finished scrolling, false otherwise. + */ + public final boolean isFinished() { + return mFinished; + } + + /** + * Force the finished field to a particular value. + * + * @param finished The new finished value. + */ + public final void forceFinished(boolean finished) { + mFinished = finished; + } + + /** + * Returns how long the scroll event will take, in milliseconds. + * + * @return The duration of the scroll in milliseconds. + */ + public final int getDuration() { + return mDuration; + } + + /** + * Returns the current X offset in the scroll. + * + * @return The new X offset as an absolute distance from the origin. + */ + public final int getCurrX() { + return mCurrX; + } + + /** + * Returns the current Y offset in the scroll. + * + * @return The new Y offset as an absolute distance from the origin. + */ + public final int getCurrY() { + return mCurrY; + } + + /** + * Returns the current velocity. + * + * @return The original velocity less the deceleration. Result may be + * negative. + */ + public float getCurrVelocity() { + return mMode == FLING_MODE ? + mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f; + } + + /** + * Returns the start X offset in the scroll. + * + * @return The start X offset as an absolute distance from the origin. + */ + public final int getStartX() { + return mStartX; + } + + /** + * Returns the start Y offset in the scroll. + * + * @return The start Y offset as an absolute distance from the origin. + */ + public final int getStartY() { + return mStartY; + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final X offset as an absolute distance from the origin. + */ + public final int getFinalX() { + return mFinalX; + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final Y offset as an absolute distance from the origin. + */ + public final int getFinalY() { + return mFinalY; + } + + /** + * Call this when you want to know the new location. If it returns true, + * the animation is not yet finished. + */ + public boolean computeScrollOffset() { + if (mFinished) { + return false; + } + + int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); + + if (timePassed < mDuration) { + switch (mMode) { + case SCROLL_MODE: + final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); + mCurrX = mStartX + Math.round(x * mDeltaX); + mCurrY = mStartY + Math.round(x * mDeltaY); + break; + case FLING_MODE: + final float t = (float) timePassed / mDuration; + final int index = (int) (NB_SAMPLES * t); + float distanceCoef = 1.f; + float velocityCoef = 0.f; + if (index < NB_SAMPLES) { + final float t_inf = (float) index / NB_SAMPLES; + final float t_sup = (float) (index + 1) / NB_SAMPLES; + final float d_inf = SPLINE_POSITION[index]; + final float d_sup = SPLINE_POSITION[index + 1]; + velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); + distanceCoef = d_inf + (t - t_inf) * velocityCoef; + } + + mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f; + + mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); + // Pin to mMinX <= mCurrX <= mMaxX + mCurrX = Math.min(mCurrX, mMaxX); + mCurrX = Math.max(mCurrX, mMinX); + + mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); + // Pin to mMinY <= mCurrY <= mMaxY + mCurrY = Math.min(mCurrY, mMaxY); + mCurrY = Math.max(mCurrY, mMinY); + + if (mCurrX == mFinalX && mCurrY == mFinalY) { + mFinished = true; + } + + break; + } + } + else { + mCurrX = mFinalX; + mCurrY = mFinalY; + mFinished = true; + } + return true; + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * The scroll will use the default value of 250 milliseconds for the + * duration. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + */ + public void startScroll(int startX, int startY, int dx, int dy) { + startScroll(startX, startY, dx, dy, DEFAULT_DURATION); + } + + /** + * Start scrolling by providing a starting point, the distance to travel, + * and the duration of the scroll. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + * @param duration Duration of the scroll in milliseconds. + */ + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + mMode = SCROLL_MODE; + mFinished = false; + mDuration = duration; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStartX = startX; + mStartY = startY; + mFinalX = startX + dx; + mFinalY = startY + dy; + mDeltaX = dx; + mDeltaY = dy; + mDurationReciprocal = 1.0f / (float) mDuration; + } + + /** + * Start scrolling based on a fling gesture. The distance travelled will + * depend on the initial velocity of the fling. + * + * @param startX Starting point of the scroll (X) + * @param startY Starting point of the scroll (Y) + * @param velocityX Initial velocity of the fling (X) measured in pixels per + * second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per + * second + * @param minX Minimum X value. The scroller will not scroll past this + * point. + * @param maxX Maximum X value. The scroller will not scroll past this + * point. + * @param minY Minimum Y value. The scroller will not scroll past this + * point. + * @param maxY Maximum Y value. The scroller will not scroll past this + * point. + */ + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY) { + // Continue a scroll or fling in progress + if (mFlywheel && !mFinished) { + float oldVel = getCurrVelocity(); + + float dx = (float) (mFinalX - mStartX); + float dy = (float) (mFinalY - mStartY); + float hyp = (float) Math.hypot(dx, dy); + + float ndx = dx / hyp; + float ndy = dy / hyp; + + float oldVelocityX = ndx * oldVel; + float oldVelocityY = ndy * oldVel; + if (Math.signum(velocityX) == Math.signum(oldVelocityX) && + Math.signum(velocityY) == Math.signum(oldVelocityY)) { + velocityX += oldVelocityX; + velocityY += oldVelocityY; + } + } + + mMode = FLING_MODE; + mFinished = false; + + float velocity = (float) Math.hypot(velocityX, velocityY); + + mVelocity = velocity; + mDuration = getSplineFlingDuration(velocity); + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStartX = startX; + mStartY = startY; + + float coeffX = velocity == 0 ? 1.0f : velocityX / velocity; + float coeffY = velocity == 0 ? 1.0f : velocityY / velocity; + + double totalDistance = getSplineFlingDistance(velocity); + mDistance = (int) (totalDistance * Math.signum(velocity)); + + mMinX = minX; + mMaxX = maxX; + mMinY = minY; + mMaxY = maxY; + + mFinalX = startX + (int) Math.round(totalDistance * coeffX); + // Pin to mMinX <= mFinalX <= mMaxX + mFinalX = Math.min(mFinalX, mMaxX); + mFinalX = Math.max(mFinalX, mMinX); + + mFinalY = startY + (int) Math.round(totalDistance * coeffY); + // Pin to mMinY <= mFinalY <= mMaxY + mFinalY = Math.min(mFinalY, mMaxY); + mFinalY = Math.max(mFinalY, mMinY); + } + + private double getSplineDeceleration(float velocity) { + return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); + } + + private int getSplineFlingDuration(float velocity) { + final double l = getSplineDeceleration(velocity); + final double decelMinusOne = DECELERATION_RATE - 1.0; + return (int) (1000.0 * Math.exp(l / decelMinusOne)); + } + + private double getSplineFlingDistance(float velocity) { + final double l = getSplineDeceleration(velocity); + final double decelMinusOne = DECELERATION_RATE - 1.0; + return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); + } + + /** + * Stops the animation. Contrary to {@link #forceFinished(boolean)}, + * aborting the animating cause the scroller to move to the final x and y + * position + * + * @see #forceFinished(boolean) + */ + public void abortAnimation() { + mCurrX = mFinalX; + mCurrY = mFinalY; + mFinished = true; + } + + /** + * Extend the scroll animation. This allows a running animation to scroll + * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. + * + * @param extend Additional time to scroll in milliseconds. + * @see #setFinalX(int) + * @see #setFinalY(int) + */ + public void extendDuration(int extend) { + int passed = timePassed(); + mDuration = passed + extend; + mDurationReciprocal = 1.0f / mDuration; + mFinished = false; + } + + /** + * Returns the time elapsed since the beginning of the scrolling. + * + * @return The elapsed time in milliseconds. + */ + public int timePassed() { + return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); + } + + /** + * Sets the final position (X) for this scroller. + * + * @param newX The new X offset as an absolute distance from the origin. + * @see #extendDuration(int) + * @see #setFinalY(int) + */ + public void setFinalX(int newX) { + mFinalX = newX; + mDeltaX = mFinalX - mStartX; + mFinished = false; + } + + /** + * Sets the final position (Y) for this scroller. + * + * @param newY The new Y offset as an absolute distance from the origin. + * @see #extendDuration(int) + * @see #setFinalX(int) + */ + public void setFinalY(int newY) { + mFinalY = newY; + mDeltaY = mFinalY - mStartY; + mFinished = false; + } + + /** + * @hide + */ + public boolean isScrollingInDirection(float xvel, float yvel) { + return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && + Math.signum(yvel) == Math.signum(mFinalY - mStartY); + } + + static class ViscousFluidInterpolator implements Interpolator { + /** Controls the viscous fluid effect (how much of it). */ + private static final float VISCOUS_FLUID_SCALE = 8.0f; + + private static final float VISCOUS_FLUID_NORMALIZE; + private static final float VISCOUS_FLUID_OFFSET; + + static { + + // must be set to 1.0 (used in viscousFluid()) + VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f); + // account for very small floating-point error + VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f); + } + + private static float viscousFluid(float x) { + x *= VISCOUS_FLUID_SCALE; + if (x < 1.0f) { + x -= (1.0f - (float)Math.exp(-x)); + } else { + float start = 0.36787944117f; // 1/e == exp(-1) + x = 1.0f - (float)Math.exp(1.0f - x); + x = start + x * (1.0f - start); + } + return x; + } + + @Override + public float getInterpolation(float input) { + final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input); + if (interpolated > 0) { + return interpolated + VISCOUS_FLUID_OFFSET; + } + return interpolated; + } + } +} \ No newline at end of file diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/utils/StringUtils.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/utils/StringUtils.kt new file mode 100644 index 0000000..92a8863 --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/utils/StringUtils.kt @@ -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 -> "" + } + } +} diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/utils/lunar/Lunar.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/utils/lunar/Lunar.kt new file mode 100644 index 0000000..e6c9522 --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/utils/lunar/Lunar.kt @@ -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] + //鍐滃巻姝f湀鐨勫亸绉� + 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() + + } + + +} \ No newline at end of file diff --git a/date_time_picker/src/main/java/com/loper7/date_time_picker/utils/lunar/LunarConstants.kt b/date_time_picker/src/main/java/com/loper7/date_time_picker/utils/lunar/LunarConstants.kt new file mode 100644 index 0000000..85caab8 --- /dev/null +++ b/date_time_picker/src/main/java/com/loper7/date_time_picker/utils/lunar/LunarConstants.kt @@ -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( + "姝f湀", "浜屾湀", "涓夋湀", "鍥涙湀", + "浜旀湀", "鍏湀", "涓冩湀", "鍏湀", "涔濇湀", "鍗佹湀", "鍐湀", "鑵婃湀" + ) + + /** 鍐滃巻鏃ュ悕绉� **/ + 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浣� 琛ㄧず 姝f湀鍒濅竴 璺濈 鍏巻1鏈�1鏃� 鐨勫ぉ鏁�: (0001 0110)2 -> 22 澶� + * 缁х画寰�鍓嶈4浣� 琛ㄧず 闂板摢涓湀 (0100)2 -> 4 鍗� 闂板洓鏈� 锛�0琛ㄧず璇ュ勾娌℃湁闂版湀锛� + * 缁х画寰�鍓嶈13浣� 琛ㄧず 姣忔湀澶╂暟淇℃伅 鍏朵腑鍓�12浣嶈〃绀烘鏈堝埌鑵婃湀鐨勫ぉ鏁颁俊鎭� 绗�13浣嶈〃绀洪棸鏈堢殑澶╂暟淇℃伅 (1 0111 0101 0100)2 -> 姝f湀澶с�佷簩鏈堝皬銆佷笁鏈堝ぇ 銆傘�傘�傝厞鏈堝皬銆侀棸鍥涙湀灏� + * + * 娉�:鍐滃巻鏈堝ぇ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 + ) +} \ No newline at end of file diff --git a/date_time_picker/src/main/res/drawable/shape_bg_oval_accent.xml b/date_time_picker/src/main/res/drawable/shape_bg_oval_accent.xml new file mode 100644 index 0000000..0ba8869 --- /dev/null +++ b/date_time_picker/src/main/res/drawable/shape_bg_oval_accent.xml @@ -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> \ No newline at end of file diff --git a/date_time_picker/src/main/res/drawable/shape_bg_round_white_5.xml b/date_time_picker/src/main/res/drawable/shape_bg_round_white_5.xml new file mode 100644 index 0000000..c50f97c --- /dev/null +++ b/date_time_picker/src/main/res/drawable/shape_bg_round_white_5.xml @@ -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> \ No newline at end of file diff --git a/date_time_picker/src/main/res/drawable/shape_bg_top_round_white_15.xml b/date_time_picker/src/main/res/drawable/shape_bg_top_round_white_15.xml new file mode 100644 index 0000000..26df806 --- /dev/null +++ b/date_time_picker/src/main/res/drawable/shape_bg_top_round_white_15.xml @@ -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> \ No newline at end of file diff --git a/date_time_picker/src/main/res/layout/dt_dialog_time_picker.xml b/date_time_picker/src/main/res/layout/dt_dialog_time_picker.xml new file mode 100644 index 0000000..32b77e1 --- /dev/null +++ b/date_time_picker/src/main/res/layout/dt_dialog_time_picker.xml @@ -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> \ No newline at end of file diff --git a/date_time_picker/src/main/res/layout/dt_dialog_week_picker.xml b/date_time_picker/src/main/res/layout/dt_dialog_week_picker.xml new file mode 100644 index 0000000..cbc2d2d --- /dev/null +++ b/date_time_picker/src/main/res/layout/dt_dialog_week_picker.xml @@ -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> \ No newline at end of file diff --git a/date_time_picker/src/main/res/layout/dt_layout_date_picker.xml b/date_time_picker/src/main/res/layout/dt_layout_date_picker.xml new file mode 100644 index 0000000..2ce1140 --- /dev/null +++ b/date_time_picker/src/main/res/layout/dt_layout_date_picker.xml @@ -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> \ No newline at end of file diff --git a/date_time_picker/src/main/res/layout/dt_layout_date_picker_globalization.xml b/date_time_picker/src/main/res/layout/dt_layout_date_picker_globalization.xml new file mode 100644 index 0000000..aa03074 --- /dev/null +++ b/date_time_picker/src/main/res/layout/dt_layout_date_picker_globalization.xml @@ -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> \ No newline at end of file diff --git a/date_time_picker/src/main/res/values/attrs.xml b/date_time_picker/src/main/res/values/attrs.xml new file mode 100644 index 0000000..2ad3c84 --- /dev/null +++ b/date_time_picker/src/main/res/values/attrs.xml @@ -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> \ No newline at end of file diff --git a/date_time_picker/src/main/res/values/colors.xml b/date_time_picker/src/main/res/values/colors.xml new file mode 100644 index 0000000..31a9e0d --- /dev/null +++ b/date_time_picker/src/main/res/values/colors.xml @@ -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> \ No newline at end of file diff --git a/date_time_picker/src/main/res/values/ids.xml b/date_time_picker/src/main/res/values/ids.xml new file mode 100644 index 0000000..9d2cd91 --- /dev/null +++ b/date_time_picker/src/main/res/values/ids.xml @@ -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> diff --git a/date_time_picker/src/main/res/values/styles.xml b/date_time_picker/src/main/res/values/styles.xml new file mode 100644 index 0000000..d884a02 --- /dev/null +++ b/date_time_picker/src/main/res/values/styles.xml @@ -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> \ No newline at end of file diff --git a/date_time_picker/src/test/java/com/loper7/date_time_picker/ExampleUnitTest.kt b/date_time_picker/src/test/java/com/loper7/date_time_picker/ExampleUnitTest.kt new file mode 100644 index 0000000..aeac68d --- /dev/null +++ b/date_time_picker/src/test/java/com/loper7/date_time_picker/ExampleUnitTest.kt @@ -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) + } +} diff --git a/settings.gradle b/settings.gradle index 861322c..3a171c1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,3 +2,4 @@ include ':app' include ':expand_button' include ':library' +include ':date_time_picker' -- Gitblit v1.8.0