From 73fea2043003d2a3a721467d0d2fad0ddb0cd131 Mon Sep 17 00:00:00 2001 From: Aminha Date: Wed, 28 Nov 2018 14:20:35 +0330 Subject: [PATCH] All widgets with custom font :astonished: You can build your application with your own font without any problem, What is that mean ? Its mean you can set your custom font on your widgets such as TextView, Button and etc with only one line code in your application class. Enjoy coding :smile: --- .gitignore | 11 + .idea/codeStyles/Project.xml | 29 + .idea/gradle.xml | 19 + .idea/misc.xml | 9 + .idea/runConfigurations.xml | 12 + .idea/vcs.xml | 6 + app/.gitignore | 1 + app/build.gradle | 33 + app/proguard-rules.pro | 21 + .../ExampleInstrumentedTest.java | 26 + app/src/main/AndroidManifest.xml | 22 + app/src/main/assets/font/BYekan.ttf | Bin 0 -> 59524 bytes .../mohammadaminha/com/widgets_package/G.java | 14 + .../com/widgets_package/MainActivity.java | 13 + .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 170 +++ app/src/main/res/layout/activity_main.xml | 14 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2963 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4905 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2060 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2783 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6895 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6387 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10413 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9128 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15132 bytes app/src/main/res/values/colors.xml | 6 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 11 + .../com/widgets_package/ExampleUnitTest.java | 17 + build.gradle | 27 + gradle.properties | 14 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 +++ gradlew.bat | 84 ++ settings.gradle | 1 + widgets/.gitignore | 1 + widgets/build.gradle | 39 + widgets/proguard-rules.pro | 21 + .../com/widgets/ExampleInstrumentedTest.java | 26 + widgets/src/main/AndroidManifest.xml | 2 + .../mohammadaminha/com/widgets/Button.java | 36 + .../mohammadaminha/com/widgets/CardView.java | 34 + .../mohammadaminha/com/widgets/CheckBox.java | 48 + .../com/widgets/Coordinator.java | 25 + .../com/widgets/CurrencyEditText.java | 179 +++ .../CustomTextView/JustifiedTextView.java | 351 ++++++ .../XmlToClassAttribHandler.java | 131 +++ .../com/widgets/CustomTypefaceSpan.java | 50 + .../CustomViewPager/AutoScrollViewPager.java | 311 +++++ .../CustomDurationScroller.java | 32 + .../Date_Picker/AccessibleLinearLayout.java | 46 + .../Date_Picker/AccessibleTextView.java | 58 + .../Date_Picker/HapticFeedbackController.java | 74 ++ .../widgets/Date_Picker/TypefaceHelper.java | 39 + .../com/widgets/Date_Picker/Utils.java | 99 ++ .../date/AccessibleDateAnimator.java | 57 + .../date/DatePickerController.java | 53 + .../Date_Picker/date/DatePickerDialog.java | 635 ++++++++++ .../Date_Picker/date/DayPickerView.java | 508 ++++++++ .../Date_Picker/date/MonthAdapter.java | 226 ++++ .../widgets/Date_Picker/date/MonthView.java | 828 +++++++++++++ .../Date_Picker/date/SimpleDayPickerView.java | 40 + .../Date_Picker/date/SimpleMonthAdapter.java | 34 + .../Date_Picker/date/SimpleMonthView.java | 62 + .../date/TextViewWithCircularIndicator.java | 89 ++ .../Date_Picker/date/YearPickerView.java | 164 +++ .../multidate/DatePickerController.java | 59 + .../Date_Picker/multidate/DayPickerView.java | 512 ++++++++ .../Date_Picker/multidate/MonthAdapter.java | 247 ++++ .../Date_Picker/multidate/MonthView.java | 836 +++++++++++++ .../multidate/MultiDatePickerDialog.java | 650 ++++++++++ .../multidate/SimpleDayPickerView.java | 40 + .../multidate/SimpleMonthAdapter.java | 34 + .../multidate/SimpleMonthView.java | 67 ++ .../Date_Picker/multidate/YearPickerView.java | 165 +++ .../Date_Picker/time/AmPmCirclesView.java | 218 ++++ .../widgets/Date_Picker/time/CircleView.java | 122 ++ .../Date_Picker/time/RadialPickerLayout.java | 861 ++++++++++++++ .../Date_Picker/time/RadialSelectorView.java | 398 +++++++ .../Date_Picker/time/RadialTextsView.java | 380 ++++++ .../Date_Picker/time/TimePickerDialog.java | 1046 +++++++++++++++++ .../Date_Picker/utils/LanguageUtils.java | 48 + .../Date_Picker/utils/PersianCalendar.java | 375 ++++++ .../utils/PersianCalendarConstants.java | 46 + .../utils/PersianCalendarUtils.java | 80 ++ .../Date_Picker/utils/PersianDateParser.java | 162 +++ .../widgets/Date_Picker/utils/TimeZones.java | 161 +++ .../mohammadaminha/com/widgets/EditText.java | 36 + .../ExpendableLayout/ExpandableLayout.java | 328 ++++++ .../util/FastOutSlowInInterpolator.java | 70 ++ .../util/LookupTableInterpolator.java | 57 + .../widgets/GridSpacingItemDecoration.java | 50 + .../widgets/ImageViewZoom/ImageViewTouch.java | 404 +++++++ .../ImageViewZoom/ImageViewTouchBase.java | 957 +++++++++++++++ .../graphics/FastBitmapDrawable.java | 110 ++ .../graphics/IBitmapDrawable.java | 14 + .../ImageViewZoom/utils/IDisposable.java | 6 + .../com/widgets/JalaliCalendar.java | 418 +++++++ .../com/widgets/JalaliCalendar1.java | 802 +++++++++++++ .../com/widgets/RadioButton.java | 35 + .../widgets/Spinner/OnSpinerItemClick.java | 9 + .../mohammadaminha/com/widgets/Switch.java | 33 + .../com/widgets/TextInputLayout.java | 34 + .../mohammadaminha/com/widgets/TextView.java | 41 + .../com/widgets/ToolbarCustomizer.java | 28 + .../java/mohammadaminha/com/widgets/Util.java | 28 + .../mohammadaminha/com/widgets/cToast.java | 40 + .../widgets/particleview/LineEvaluator.java | 19 + .../com/widgets/particleview/Particle.java | 21 + .../widgets/particleview/ParticleView.java | 385 ++++++ .../res/color/mdtp_date_picker_selector.xml | 23 + .../color/mdtp_date_picker_year_selector.xml | 23 + .../main/res/color/mdtp_done_text_color.xml | 21 + .../res/color/mdtp_done_text_color_dark.xml | 21 + .../drawable/mdtp_done_background_color.xml | 25 + .../mdtp_done_background_color_dark.xml | 25 + .../mdtp_material_button_background.xml | 6 + .../mdtp_material_button_selected.xml | 13 + .../res/layout/mdtp_date_picker_dialog.xml | 36 + .../layout/mdtp_date_picker_header_view.xml | 26 + .../layout/mdtp_date_picker_selected_date.xml | 72 ++ .../layout/mdtp_date_picker_view_animator.xml | 21 + .../src/main/res/layout/mdtp_done_button.xml | 41 + .../res/layout/mdtp_time_header_label.xml | 122 ++ .../res/layout/mdtp_time_picker_dialog.xml | 60 + .../res/layout/mdtp_year_label_text_view.xml | 23 + widgets/src/main/res/values-v21/styles.xml | 15 + widgets/src/main/res/values/attrs.xml | 86 ++ widgets/src/main/res/values/colors.xml | 76 ++ widgets/src/main/res/values/dimens.xml | 61 + widgets/src/main/res/values/strings.xml | 713 +++++++++++ widgets/src/main/res/values/styles.xml | 288 +++++ .../com/widgets/ExampleUnitTest.java | 17 + 138 files changed, 17328 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/mohammadaminha/com/widgets_package/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/assets/font/BYekan.ttf create mode 100644 app/src/main/java/mohammadaminha/com/widgets_package/G.java create mode 100644 app/src/main/java/mohammadaminha/com/widgets_package/MainActivity.java create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/mohammadaminha/com/widgets_package/ExampleUnitTest.java create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 widgets/.gitignore create mode 100644 widgets/build.gradle create mode 100644 widgets/proguard-rules.pro create mode 100644 widgets/src/androidTest/java/mohammadaminha/com/widgets/ExampleInstrumentedTest.java create mode 100644 widgets/src/main/AndroidManifest.xml create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Button.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/CardView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/CheckBox.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Coordinator.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/CurrencyEditText.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/CustomTextView/JustifiedTextView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/CustomTextView/XmlToClassAttribHandler.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/CustomTypefaceSpan.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/CustomViewPager/AutoScrollViewPager.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/CustomViewPager/CustomDurationScroller.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/AccessibleLinearLayout.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/AccessibleTextView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/HapticFeedbackController.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/TypefaceHelper.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/Utils.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/AccessibleDateAnimator.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/DatePickerController.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/DatePickerDialog.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/DayPickerView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/MonthAdapter.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/MonthView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/SimpleDayPickerView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/SimpleMonthAdapter.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/SimpleMonthView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/TextViewWithCircularIndicator.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/YearPickerView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/DatePickerController.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/DayPickerView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/MonthAdapter.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/MonthView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/MultiDatePickerDialog.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/SimpleDayPickerView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/SimpleMonthAdapter.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/SimpleMonthView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/YearPickerView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/AmPmCirclesView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/CircleView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/RadialPickerLayout.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/RadialSelectorView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/RadialTextsView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/TimePickerDialog.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/LanguageUtils.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianCalendar.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianCalendarConstants.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianCalendarUtils.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianDateParser.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/TimeZones.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/EditText.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/ExpendableLayout/ExpandableLayout.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/ExpendableLayout/util/FastOutSlowInInterpolator.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/ExpendableLayout/util/LookupTableInterpolator.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/GridSpacingItemDecoration.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/ImageViewTouch.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/ImageViewTouchBase.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/graphics/FastBitmapDrawable.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/graphics/IBitmapDrawable.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/utils/IDisposable.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/JalaliCalendar.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/JalaliCalendar1.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/RadioButton.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Spinner/OnSpinerItemClick.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Switch.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/TextInputLayout.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/TextView.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/ToolbarCustomizer.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/Util.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/cToast.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/particleview/LineEvaluator.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/particleview/Particle.java create mode 100644 widgets/src/main/java/mohammadaminha/com/widgets/particleview/ParticleView.java create mode 100644 widgets/src/main/res/color/mdtp_date_picker_selector.xml create mode 100644 widgets/src/main/res/color/mdtp_date_picker_year_selector.xml create mode 100644 widgets/src/main/res/color/mdtp_done_text_color.xml create mode 100644 widgets/src/main/res/color/mdtp_done_text_color_dark.xml create mode 100644 widgets/src/main/res/drawable/mdtp_done_background_color.xml create mode 100644 widgets/src/main/res/drawable/mdtp_done_background_color_dark.xml create mode 100644 widgets/src/main/res/drawable/mdtp_material_button_background.xml create mode 100644 widgets/src/main/res/drawable/mdtp_material_button_selected.xml create mode 100644 widgets/src/main/res/layout/mdtp_date_picker_dialog.xml create mode 100644 widgets/src/main/res/layout/mdtp_date_picker_header_view.xml create mode 100644 widgets/src/main/res/layout/mdtp_date_picker_selected_date.xml create mode 100644 widgets/src/main/res/layout/mdtp_date_picker_view_animator.xml create mode 100644 widgets/src/main/res/layout/mdtp_done_button.xml create mode 100644 widgets/src/main/res/layout/mdtp_time_header_label.xml create mode 100644 widgets/src/main/res/layout/mdtp_time_picker_dialog.xml create mode 100644 widgets/src/main/res/layout/mdtp_year_label_text_view.xml create mode 100644 widgets/src/main/res/values-v21/styles.xml create mode 100644 widgets/src/main/res/values/attrs.xml create mode 100644 widgets/src/main/res/values/colors.xml create mode 100644 widgets/src/main/res/values/dimens.xml create mode 100644 widgets/src/main/res/values/strings.xml create mode 100644 widgets/src/main/res/values/styles.xml create mode 100644 widgets/src/test/java/mohammadaminha/com/widgets/ExampleUnitTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd45b12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +/.idea/caches/build_file_checksums.ser +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..30aa626 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..09556ab --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..37a7509 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..0af46ce --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,33 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 27 + defaultConfig { + applicationId "mohammadaminha.com.widgets_package" + minSdkVersion 17 + targetSdkVersion 27 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support:support-v4:27.1.1' + implementation 'com.android.support:design:27.1.1' + implementation 'com.android.support:recyclerview-v7:27.1.1' + implementation 'com.github.hotchemi:khronos:0.9.0' + implementation 'com.android.support:cardview-v7:27.1.1' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:0.5' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2' + implementation project(':widgets') +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/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/app/src/androidTest/java/mohammadaminha/com/widgets_package/ExampleInstrumentedTest.java b/app/src/androidTest/java/mohammadaminha/com/widgets_package/ExampleInstrumentedTest.java new file mode 100644 index 0000000..2dd160d --- /dev/null +++ b/app/src/androidTest/java/mohammadaminha/com/widgets_package/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package mohammadaminha.com.widgets_package; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("mohammadaminha.com.widgets_package", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8e11841 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/font/BYekan.ttf b/app/src/main/assets/font/BYekan.ttf new file mode 100644 index 0000000000000000000000000000000000000000..168a908e545d7fd972fb4542e5129074592f27ee GIT binary patch literal 59524 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 g|KZ9w-F(JQ=Y?tL`!pRLlQ^rh>Szr(# + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..7c22d2b --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..898f3ed59ac9f3248734a00e5902736c9367d455 GIT binary patch literal 2963 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 JPDHLkV1g@XpTGbB literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..dffca3601eba7bf5f409bdd520820e2eb5122c75 GIT binary patch literal 4905 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 lV%Z2*hu);6rydA(}wUDXPCF_W1vnaRBK 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..14ed0af35023e4f1901cf03487b6c524257b8483 GIT binary patch literal 6895 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b0907cac3bfd8fbfdc46e1108247f0a1055387ec GIT binary patch literal 6387 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ae03154975f397f8ed1b84f2d4bf9783ecfa26 GIT binary patch literal 10413 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 XdJ+W}DiIkV00000NkvXXu0mjfKBlg6 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2c18de9e66108411737e910f5c1972476f03ddbf GIT binary patch literal 9128 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 I20H400LcNok^lez literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..beed3cdd2c32af5114a7dc70b9ef5b698eb8797e GIT binary patch literal 15132 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..69b2233 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #008577 + #00574B + #D81B60 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..711e60d --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Widgets_Package + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..5885930 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/test/java/mohammadaminha/com/widgets_package/ExampleUnitTest.java b/app/src/test/java/mohammadaminha/com/widgets_package/ExampleUnitTest.java new file mode 100644 index 0000000..f499593 --- /dev/null +++ b/app/src/test/java/mohammadaminha/com/widgets_package/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package mohammadaminha.com.widgets_package; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..4e8009d --- /dev/null +++ b/build.gradle @@ -0,0 +1,27 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.2.0' + + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1487463 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,14 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

<5G2M1WZP5 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 F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9a4163a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..55c2069 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app', ':widgets' diff --git a/widgets/.gitignore b/widgets/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/widgets/.gitignore @@ -0,0 +1 @@ +/build diff --git a/widgets/build.gradle b/widgets/build.gradle new file mode 100644 index 0000000..ac08554 --- /dev/null +++ b/widgets/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 27 + + + + defaultConfig { + minSdkVersion 17 + targetSdkVersion 27 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support:support-v4:27.1.1' + implementation 'com.android.support:design:27.1.1' + implementation 'com.android.support:recyclerview-v7:27.1.1' + implementation 'com.github.hotchemi:khronos:0.9.0' + implementation 'com.android.support:cardview-v7:27.1.1' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:0.5' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2' +} diff --git a/widgets/proguard-rules.pro b/widgets/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/widgets/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/widgets/src/androidTest/java/mohammadaminha/com/widgets/ExampleInstrumentedTest.java b/widgets/src/androidTest/java/mohammadaminha/com/widgets/ExampleInstrumentedTest.java new file mode 100644 index 0000000..906797e --- /dev/null +++ b/widgets/src/androidTest/java/mohammadaminha/com/widgets/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package mohammadaminha.com.widgets; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("mohammadaminha.com.widgets.test", appContext.getPackageName()); + } +} diff --git a/widgets/src/main/AndroidManifest.xml b/widgets/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b54e73e --- /dev/null +++ b/widgets/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Button.java b/widgets/src/main/java/mohammadaminha/com/widgets/Button.java new file mode 100644 index 0000000..e01a792 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Button.java @@ -0,0 +1,36 @@ +package mohammadaminha.com.widgets; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * Created by aj on 2/1/2018. + */ + +public class Button extends android.support.v7.widget.AppCompatButton{ + + + public Button(Context context) { + super(context); + + setTf(context); + } + + + public Button(Context context, AttributeSet attrs) { + super(context, attrs); + setTf(context); + } + + public Button(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setTf(context); + } + + + private void setTf(Context context) { + setTypeface(Util.getTypeFace()); + } + + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/CardView.java b/widgets/src/main/java/mohammadaminha/com/widgets/CardView.java new file mode 100644 index 0000000..54e7edb --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/CardView.java @@ -0,0 +1,34 @@ +package mohammadaminha.com.widgets; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; + +public class CardView extends android.support.v7.widget.CardView { + public CardView(@NonNull Context context) { + super(context); + Customizer(context); + } + + public CardView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + Customizer(context); + } + + public CardView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + Customizer(context); + + } + + private void Customizer(Context context) { + setLayoutDirection(LAYOUT_DIRECTION_RTL); + setTextDirection(TEXT_DIRECTION_RTL); + setPadding(6, 6, 6, 6); + setCardElevation(12f); + setUseCompatPadding(true); + } + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/CheckBox.java b/widgets/src/main/java/mohammadaminha/com/widgets/CheckBox.java new file mode 100644 index 0000000..09c47a2 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/CheckBox.java @@ -0,0 +1,48 @@ +package mohammadaminha.com.widgets; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.support.v4.content.ContextCompat; +import android.support.v4.widget.CompoundButtonCompat; +import android.util.AttributeSet; + +/** + * Created by aj on 2/1/2018. + */ + +public class CheckBox extends android.support.v7.widget.AppCompatCheckBox { + + + public CheckBox(Context context) { + super(context); + setTf(context); + } + + public CheckBox(Context context, AttributeSet attrs) { + super(context, attrs); + setTf(context); + } + + public CheckBox(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setTf(context); + } + + private void setTf(Context context) { + + ColorStateList colorStateList = new ColorStateList( + new int[][]{ + new int[]{-android.R.attr.state_checked}, // unchecked + new int[]{android.R.attr.state_checked} , // checked + }, + new int[]{ + ContextCompat.getColor(context,R.color.GrayColor), //unchecked color + ContextCompat.getColor(context,R.color.YellowColor), //checked color + } + ); + CompoundButtonCompat.setButtonTintList(this,colorStateList); + setTypeface(Util.getTypeFace()); + } + + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Coordinator.java b/widgets/src/main/java/mohammadaminha/com/widgets/Coordinator.java new file mode 100644 index 0000000..3354dc8 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Coordinator.java @@ -0,0 +1,25 @@ +package mohammadaminha.com.widgets; + +import android.content.Context; +import android.support.design.widget.CoordinatorLayout; +import android.util.AttributeSet; + +public class Coordinator extends CoordinatorLayout { + public Coordinator(Context context) { + super(context); + Customizer(); + } + + public Coordinator(Context context, AttributeSet attrs) { + super(context, attrs); + Customizer(); + } + + public Coordinator(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + Customizer(); + } + private void Customizer(){ + setLayoutDirection(LAYOUT_DIRECTION_RTL); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/CurrencyEditText.java b/widgets/src/main/java/mohammadaminha/com/widgets/CurrencyEditText.java new file mode 100644 index 0000000..c465cb7 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/CurrencyEditText.java @@ -0,0 +1,179 @@ +package mohammadaminha.com.widgets; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Editable; +import android.text.InputFilter; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.widget.EditText; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +/** + * Created by PhanVanLinh on 25/07/2017. + * phanvanlinh.94vn@gmail.com + *

+ * Some note
+ *

  • Always use locale US instead of default to make DecimalFormat work well in all language
  • + */ +public class CurrencyEditText extends android.support.v7.widget.AppCompatEditText { + private static String prefix = ""; + private static final int MAX_LENGTH = 30; + private static final int MAX_DECIMAL = 3; + private CurrencyTextWatcher currencyTextWatcher = new CurrencyTextWatcher(this, prefix); + + + public CurrencyEditText(Context context) { + this(context, null); + setTf(context); + } + + public CurrencyEditText(Context context, AttributeSet attrs) { + this(context, attrs, android.support.v7.appcompat.R.attr.editTextStyle); + setTf(context); + } + + public CurrencyEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + this.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); + this.setHint(prefix); + this.setFilters(new InputFilter[]{new InputFilter.LengthFilter(MAX_LENGTH)}); + setTf(context); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + if (focused) { + this.addTextChangedListener(currencyTextWatcher); + } else { + this.removeTextChangedListener(currencyTextWatcher); + } + handleCaseCurrencyEmpty(focused); + } + + /** + * When currency empty
    + * + When focus EditText, set the default text = prefix (ex: VND)
    + * + When EditText lose focus, set the default text = "", EditText will display hint (ex:VND) + */ + private void handleCaseCurrencyEmpty(boolean focused) { + if (focused) { + if (getText().toString().isEmpty()) { + setText(prefix); + } + } else { + if (getText().toString().equals(prefix)) { + setText(""); + } + } + } + + private static class CurrencyTextWatcher implements TextWatcher { + private final EditText editText; + private String previousCleanString; + private String prefix; + + CurrencyTextWatcher(EditText editText, String prefix) { + this.editText = editText; + this.prefix = prefix; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // do nothing + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // do nothing + } + + @Override + public void afterTextChanged(Editable editable) { + String str = editable.toString(); + if (str.length() < prefix.length()) { + editText.setText(prefix); + editText.setSelection(prefix.length()); + return; + } + if (str.equals(prefix)) { + return; + } + // cleanString this the string which not contain prefix and , + String cleanString = str.replace(prefix, "").replaceAll("[,]", ""); + // for prevent afterTextChanged recursive call + if (cleanString.equals(previousCleanString) || cleanString.isEmpty()) { + return; + } + previousCleanString = cleanString; + + String formattedString; + if (cleanString.contains(".")) { + formattedString = formatDecimal(cleanString); + } else { + formattedString = formatInteger(cleanString); + } + editText.removeTextChangedListener(this); // Remove listener + editText.setText(formattedString); + handleSelection(); + editText.addTextChangedListener(this); // Add back the listener + } + + private String formatInteger(String str) { + BigDecimal parsed = new BigDecimal(str); + DecimalFormat formatter = + new DecimalFormat(prefix + "#,###", new DecimalFormatSymbols(Locale.US)); + return formatter.format(parsed); + } + + private String formatDecimal(String str) { + if (str.equals(".")) { + return prefix + "."; + } + BigDecimal parsed = new BigDecimal(str); + // example pattern VND #,###.00 + DecimalFormat formatter = new DecimalFormat(prefix + "#,###." + + getDecimalPattern(str), + new DecimalFormatSymbols(Locale.US)); + formatter.setRoundingMode(RoundingMode.DOWN); + return formatter.format(parsed); + } + + /** + * It will return suitable pattern for format decimal + * For example: 10.2 -> return 0 | 10.23 -> return 00, | 10.235 -> return 000 + */ + private String getDecimalPattern(String str) { + int decimalCount = str.length() - str.indexOf(".") - 1; + StringBuilder decimalPattern = new StringBuilder(); + for (int i = 0; i < decimalCount && i < MAX_DECIMAL; i++) { + decimalPattern.append("0"); + } + return decimalPattern.toString(); + } + + private void handleSelection() { + if (editText.getText().length() <= MAX_LENGTH) { + editText.setSelection(editText.getText().length()); + } else { + editText.setSelection(MAX_LENGTH); + } + } + } + + + private void setTf(Context context) { + + setTypeface(Util.getTypeFace()); + } + + + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/CustomTextView/JustifiedTextView.java b/widgets/src/main/java/mohammadaminha/com/widgets/CustomTextView/JustifiedTextView.java new file mode 100644 index 0000000..27c50e6 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/CustomTextView/JustifiedTextView.java @@ -0,0 +1,351 @@ +package mohammadaminha.com.widgets.CustomTextView; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint.Align; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; + +import java.util.ArrayList; +import java.util.List; + +public class JustifiedTextView extends View { + + /** + * when we want to draw text after view created to avoid loop in drawing we use this boolean + */ + private boolean hasTextBeenDrown = false; + private Context mContext; + private TextPaint textPaint; + private int lineSpace = 0; + private int lineHeight; + private int textAreaWidth; + private int measuredViewHeight, measuredViewWidth; + private String text; + private List lineList = new ArrayList<>(); + + public JustifiedTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + constructor(context, attrs); + } + + public JustifiedTextView(Context context, AttributeSet attrs) { + super(context, attrs); + constructor(context, attrs); + } + + public JustifiedTextView(Context context) { + super(context); + constructor(context, null); + + } + + private void constructor(Context context, AttributeSet attrs) { + + mContext = context; + XmlToClassAttribHandler mXmlParser = new XmlToClassAttribHandler(mContext, attrs); + initTextPaint(); + + if (attrs != null) { + String text; + int textColor; + int textSize; + int textSizeUnit; + + text = mXmlParser.getTextValue(); + textColor = mXmlParser.getColorValue(); + textSize = mXmlParser.getTextSize(); + textSizeUnit = mXmlParser.gettextSizeUnit(); + + + setText(text); + setTextColor(textColor); + if (textSizeUnit == -1) + setTextSize(textSize); + else + setTextSize(textSizeUnit, textSize); + +// setText(XmlToClassAttribHandler.GetAttributeStringValue(mContext, attrs, namespace, key, "")); + + } + + ViewTreeObserver observer = getViewTreeObserver(); + + + observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { + + @Override + public void onGlobalLayout() { + + if (hasTextBeenDrown) + return; + hasTextBeenDrown = true; + setTextAreaWidth(getWidth() - (getPaddingLeft() + getPaddingRight())); + calculate(); + + } + + + }); + + } + + private void calculate() { + setLineHeight(getTextPaint()); + lineList.clear(); + lineList = divideOriginalTextToStringLineList(getText()); + setMeasuredDimentions(lineList.size(), getLineHeight(), getLineSpace()); + measure(getMeasuredViewWidth(), getMeasuredViewHeight()); + } + + private void initTextPaint() { + textPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); + textPaint.setTextAlign(Align.RIGHT); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (getMeasuredViewWidth() > 0) { + requestLayout(); + setMeasuredDimension(getMeasuredViewWidth(), getMeasuredViewHeight()); + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + + int rowIndex = getPaddingTop(); + int colIndex ; + if (getAlignment() == Align.RIGHT) + colIndex = getPaddingLeft() + getTextAreaWidth(); + else + colIndex = getPaddingLeft(); + + for (int i = 0; i < lineList.size(); i++) { + rowIndex += getLineHeight() + getLineSpace(); + + canvas.drawText(lineList.get(i), colIndex, rowIndex, getTextPaint()); + } + + } + + + private List divideOriginalTextToStringLineList(String originalText) { + + List listStringLine = new ArrayList<>(); + + String line = ""; + float textWidth; + + String[] listParageraphes = originalText.split("\n"); + + for (String listParageraphe : listParageraphes) { + String[] arrayWords = listParageraphe.split(" "); + + for (int i = 0; i < arrayWords.length; i++) { + + line += arrayWords[i] + " "; + textWidth = getTextPaint().measureText(line); + + //if text width is equal to textAreaWidth then just add it to ListStringLine + if (getTextAreaWidth() == textWidth) { + + listStringLine.add(line); + line = "";//make line clear + continue; + } + //else if text width excite textAreaWidth then remove last word and justify the StringLine + else if (getTextAreaWidth() < textWidth) { + + int lastWordCount = arrayWords[i].length(); + + //remove last word that cause line width to excite textAreaWidth + line = line.substring(0, line.length() - lastWordCount - 1); + + // if line is empty then should be skipped + if (line.trim().length() == 0) + continue; + + //and then we need to justify line + line = justifyTextLine(textPaint, line.trim(), getTextAreaWidth()); + + listStringLine.add(line); + line = ""; + i--; + continue; + } + + //if we are now at last line of paragraph then just add it + if (i == arrayWords.length - 1) { + listStringLine.add(line); + line = ""; + } + } + } + + return listStringLine; + + } + + private String justifyTextLine(TextPaint textPaint, String lineString, int textAreaWidth) { + + int gapIndex = 0; + + float lineWidth = textPaint.measureText(lineString); + + while (lineWidth < textAreaWidth && lineWidth > 0) { + + gapIndex = lineString.indexOf(" ", gapIndex + 2); + if (gapIndex == -1) { + gapIndex = 0; + gapIndex = lineString.indexOf(" ", gapIndex + 1); + if (gapIndex == -1) + return lineString; + } + + lineString = lineString.substring(0, gapIndex) + " " + lineString.substring(gapIndex + 1, lineString.length()); + + lineWidth = textPaint.measureText(lineString); + } + return lineString; + } + + private void setLineHeight(TextPaint textPaint) { + + Rect bounds = new Rect(); + String sampleStr = ""; + textPaint.getTextBounds(sampleStr, 0, sampleStr.length(), bounds); + + setLineHeight(bounds.height()); + + } + + private void setMeasuredDimentions(int lineListSize, int lineHeigth, int lineSpace) { + int mHeight = lineListSize * (lineHeigth + lineSpace) + lineSpace; + + mHeight += getPaddingRight() + getPaddingLeft(); + + setMeasuredViewHeight(mHeight); + + setMeasuredViewWidth(getWidth()); + } + + + private int getTextAreaWidth() { + return textAreaWidth; + } + + private void setTextAreaWidth(int textAreaWidth) { + this.textAreaWidth = textAreaWidth; + } + + private int getLineHeight() { + return lineHeight; + } + + private void setLineHeight(int lineHeight) { + this.lineHeight = lineHeight; + } + + private int getMeasuredViewHeight() { + return measuredViewHeight; + } + + private void setMeasuredViewHeight(int measuredViewHeight) { + this.measuredViewHeight = measuredViewHeight; + } + + private int getMeasuredViewWidth() { + return measuredViewWidth; + } + + private void setMeasuredViewWidth(int measuredViewWidth) { + this.measuredViewWidth = measuredViewWidth; + } + + private String getText() { + return text; + } + + public void setText(int resid) { + setText(mContext.getResources().getString(resid)); + } + + private void setText(String text) { + this.text = text; + calculate(); + invalidate(); + } + + public Typeface getTypeFace() { + return getTextPaint().getTypeface(); + } + + public void setTypeFace(Typeface typeFace) { + getTextPaint().setTypeface(typeFace); + } + + public float getTextSize() { + return getTextPaint().getTextSize(); + } + + private void setTextSize(float textSize) { + getTextPaint().setTextSize(textSize); + calculate(); + invalidate(); + } + + private void setTextSize(int unit, float textSize) { + textSize = TypedValue.applyDimension(unit, textSize, mContext.getResources().getDisplayMetrics()); + setTextSize(textSize); + } + + private TextPaint getTextPaint() { + return textPaint; + } + + public void setTextPaint(TextPaint textPaint) { + this.textPaint = textPaint; + } + + public void setLineSpacing(int lineSpace) { + this.lineSpace = lineSpace; + invalidate(); + } + + /*** + * @return text color + */ + public int getTextColor() { + return getTextPaint().getColor(); + } + + private void setTextColor(int textColor) { + getTextPaint().setColor(textColor); + invalidate(); + } + + private int getLineSpace() { + return lineSpace; + } + + private Align getAlignment() { + return getTextPaint().getTextAlign(); + } + + public void setAlignment(Align align) { + getTextPaint().setTextAlign(align); + invalidate(); + } + + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/CustomTextView/XmlToClassAttribHandler.java b/widgets/src/main/java/mohammadaminha/com/widgets/CustomTextView/XmlToClassAttribHandler.java new file mode 100644 index 0000000..64dbf7e --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/CustomTextView/XmlToClassAttribHandler.java @@ -0,0 +1,131 @@ +package mohammadaminha.com.widgets.CustomTextView; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Color; +import android.util.AttributeSet; +import android.util.TypedValue; + +class XmlToClassAttribHandler { + private final String KEY_TEXT_SIZE = "textSize"; + private final Resources mRes; + private final Context mContext; + private final AttributeSet mAttributeSet; + private final String namespace = "http://noghteh.ir"; + + public XmlToClassAttribHandler(Context context, AttributeSet attributeSet) { + mContext = context; + mRes = mContext.getResources(); + mAttributeSet = attributeSet; + } + + public String getTextValue() { + + String KEY_TEXT = "text"; + String value = mAttributeSet.getAttributeValue(namespace, KEY_TEXT); + + if (value == null) + return ""; + + if (value.length() > 1 && + value.charAt(0) == '@' && + value.contains("@string/")) { + int resId = mRes.getIdentifier(mContext.getPackageName() + ":" + value.substring(1), null, null); + value = mRes.getString(resId); + } + + return value; + + } + + public int getColorValue() { + + String KEY_TEXT_COLOR = "textColor"; + String value = mAttributeSet.getAttributeValue(namespace, KEY_TEXT_COLOR); + + int color = Color.BLACK; + + if (value == null) + return color; + + if (value.length() > 1 && + value.charAt(0) == '@' && + value.contains("@color/")) { + int resId = mRes.getIdentifier(mContext.getPackageName() + ":" + value.substring(1), null, null); + color = mRes.getColor(resId); + + return color; + } + + + try { + color = Color.parseColor(value); + } catch (Exception e) { + return Color.BLACK; + } + + + return color; + } + + + public int getTextSize() { + int textSize = 12; + + String value = mAttributeSet.getAttributeValue(namespace, KEY_TEXT_SIZE); + + if (value == null) + return textSize; + + if (value.length() > 1 && + value.charAt(0) == '@' && + value.contains("@dimen/")) { + int resId = mRes.getIdentifier(mContext.getPackageName() + ":" + value.substring(1), null, null); + textSize = mRes.getDimensionPixelSize(resId); + + return textSize; + } + + try { + textSize = Integer.parseInt(value.substring(0, value.length() - 2)); + } catch (Exception e) { + return 12; + } + + return textSize; + } + + + public int gettextSizeUnit() { + + String value = mAttributeSet.getAttributeValue(namespace, KEY_TEXT_SIZE); + + if (value == null) + return TypedValue.COMPLEX_UNIT_SP; + + try { + String type = value.substring(value.length() - 2, value.length()); + + switch (type) { + case "dp": + return TypedValue.COMPLEX_UNIT_DIP; + case "sp": + return TypedValue.COMPLEX_UNIT_SP; + case "pt": + return TypedValue.COMPLEX_UNIT_PT; + case "mm": + return TypedValue.COMPLEX_UNIT_MM; + case "in": + return TypedValue.COMPLEX_UNIT_IN; + case "px": + return TypedValue.COMPLEX_UNIT_PX; + } + + } catch (Exception e) { + return -1; + } + + return -1; + } + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/CustomTypefaceSpan.java b/widgets/src/main/java/mohammadaminha/com/widgets/CustomTypefaceSpan.java new file mode 100644 index 0000000..6a99404 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/CustomTypefaceSpan.java @@ -0,0 +1,50 @@ +package mohammadaminha.com.widgets; + +import android.graphics.Paint; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.text.style.TypefaceSpan; + +/** + * Created by amin on 1/18/18. + */ + +public class CustomTypefaceSpan extends TypefaceSpan { + private final Typeface newType; + + public CustomTypefaceSpan(String family, Typeface type) { + super(family); + newType = type; + } + + @Override + public void updateDrawState(TextPaint ds) { + applyCustomTypeFace(ds, newType); + } + + @Override + public void updateMeasureState(TextPaint paint) { + applyCustomTypeFace(paint, newType); + } + + private static void applyCustomTypeFace(Paint paint, Typeface tf) { + int oldStyle; + Typeface old = paint.getTypeface(); + if (old == null) { + oldStyle = 0; + } else { + oldStyle = old.getStyle(); + } + + int fake = oldStyle & ~tf.getStyle(); + if ((fake & Typeface.BOLD) != 0) { + paint.setFakeBoldText(true); + } + + if ((fake & Typeface.ITALIC) != 0) { + paint.setTextSkewX(-0.25f); + } + + paint.setTypeface(Util.getTypeFace()); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/CustomViewPager/AutoScrollViewPager.java b/widgets/src/main/java/mohammadaminha/com/widgets/CustomViewPager/AutoScrollViewPager.java new file mode 100644 index 0000000..3f7026d --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/CustomViewPager/AutoScrollViewPager.java @@ -0,0 +1,311 @@ +package mohammadaminha.com.widgets.CustomViewPager; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.animation.Interpolator; + +import java.lang.reflect.Field; + +public class AutoScrollViewPager extends ViewPager { + + private static final int DEFAULT_INTERVAL = 4500; + + private static final int LEFT = 0; + private static final int RIGHT = 1; + + /** do nothing when sliding at the last or first item **/ + private static final int SLIDE_BORDER_MODE_NONE = 0; + /** cycle when sliding at the last or first item **/ + private static final int SLIDE_BORDER_MODE_CYCLE = 1; + /** deliver event to parent when sliding at the last or first item **/ + private static final int SLIDE_BORDER_MODE_TO_PARENT = 2; + + /** auto scroll time in milliseconds, default is {@link #DEFAULT_INTERVAL} **/ + private long interval = DEFAULT_INTERVAL; + /** auto scroll direction, default is {@link #RIGHT} **/ + private int direction = RIGHT; + /** whether automatic cycle when auto scroll reaching the last or first item, default is true **/ + private boolean isCycle = true; + /** whether stop auto scroll when touching, default is true **/ + private boolean stopScrollWhenTouch = true; + /** how to process when sliding at the last or first item, default is {@link #SLIDE_BORDER_MODE_NONE} **/ + private int slideBorderMode = SLIDE_BORDER_MODE_NONE; + /** whether animating when auto scroll at the last or first item **/ + private boolean isBorderAnimation = true; + /** scroll factor for auto scroll animation, default is 1.0 **/ + private double autoScrollFactor = 1.0; + /** scroll factor for swipe scroll animation, default is 1.0 **/ + private double swipeScrollFactor = 1.0; + + private Handler handler; + private boolean isAutoScroll = false; + private boolean isStopByTouch = false; + private float touchX = 0f; + private float downX = 0f; + private CustomDurationScroller scroller = null; + + private static final int SCROLL_WHAT = 0; + + public AutoScrollViewPager(Context paramContext) { + super(paramContext); + init(); + } + + public AutoScrollViewPager(Context paramContext, AttributeSet paramAttributeSet) { + super(paramContext, paramAttributeSet); + init(); + } + + private void init() { + handler = new MyHandler(); + setViewPagerScroller(); + } + + /** + * start auto scroll, first scroll delay time is {@link #getInterval()} + */ + public void startAutoScroll() { + isAutoScroll = true; + sendScrollMessage((long) (interval + scroller.getDuration()/ autoScrollFactor * swipeScrollFactor)); + } + + /** + * start auto scroll + * + * @param delayTimeInMills first scroll delay time + */ + public void startAutoScroll(int delayTimeInMills) { + isAutoScroll = true; + sendScrollMessage(delayTimeInMills); + } + + /** + * stop auto scroll + */ + private void stopAutoScroll() { + isAutoScroll = false; + handler.removeMessages(SCROLL_WHAT); + } + + /** + * set the factor by which the duration of sliding animation will change while swiping + */ + public void setSwipeScrollDurationFactor(double scrollFactor) { + swipeScrollFactor = scrollFactor; + } + + /** + * set the factor by which the duration of sliding animation will change while auto scrolling + */ + public void setAutoScrollDurationFactor(double scrollFactor) { + autoScrollFactor = scrollFactor; + } + + private void sendScrollMessage(long delayTimeInMills) { + handler.removeMessages(SCROLL_WHAT); + handler.sendEmptyMessageDelayed(SCROLL_WHAT, delayTimeInMills); + } + + private void setViewPagerScroller() { + try { + Field scrollerField = ViewPager.class.getDeclaredField("mScroller"); + scrollerField.setAccessible(true); + Field interpolatorField = ViewPager.class.getDeclaredField("sInterpolator"); + interpolatorField.setAccessible(true); + + scroller = new CustomDurationScroller(getContext(), (Interpolator)interpolatorField.get(null)); + scrollerField.set(this, scroller); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * scroll only once + */ + private void scrollOnce() { + PagerAdapter adapter = getAdapter(); + int currentItem = getCurrentItem(); + int totalCount; + if (adapter == null || (totalCount = adapter.getCount()) <= 1) { + return; + } + + int nextItem = (direction == LEFT) ? --currentItem : ++currentItem; + if (nextItem < 0) { + if (isCycle) { + setCurrentItem(totalCount - 1, isBorderAnimation); + } + } else if (nextItem == totalCount) { + if (isCycle) { + setCurrentItem(0, isBorderAnimation); + } + } else { + setCurrentItem(nextItem, true); + } + } + + /** + *
      + * if stopScrollWhenTouch is true + *
    • if event is down, stop auto scroll.
    • + *
    • if event is up, start auto scroll again.
    • + *
    + */ + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + int action = MotionEventCompat.getActionMasked(ev); + + if (stopScrollWhenTouch) { + if ((action == MotionEvent.ACTION_DOWN) && isAutoScroll) { + isStopByTouch = true; + stopAutoScroll(); + } else if (ev.getAction() == MotionEvent.ACTION_UP && isStopByTouch) { + startAutoScroll(); + } + } + + if (slideBorderMode == SLIDE_BORDER_MODE_TO_PARENT || slideBorderMode == SLIDE_BORDER_MODE_CYCLE) { + touchX = ev.getX(); + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + downX = touchX; + } + int currentItem = getCurrentItem(); + PagerAdapter adapter = getAdapter(); + int pageCount = adapter == null ? 0 : adapter.getCount(); + if ((currentItem == 0 && downX <= touchX) || (currentItem == pageCount - 1 && downX >= touchX)) { + if (slideBorderMode == SLIDE_BORDER_MODE_TO_PARENT) { + getParent().requestDisallowInterceptTouchEvent(false); + } else { + if (pageCount > 1) { + setCurrentItem(pageCount - currentItem - 1, isBorderAnimation); + } + getParent().requestDisallowInterceptTouchEvent(true); + } + return super.dispatchTouchEvent(ev); + } + } + getParent().requestDisallowInterceptTouchEvent(true); + + return super.dispatchTouchEvent(ev); + } + + private class MyHandler extends Handler { + + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + + switch (msg.what) { + case SCROLL_WHAT: + scroller.setScrollDurationFactor(autoScrollFactor); + scrollOnce(); + scroller.setScrollDurationFactor(swipeScrollFactor); + sendScrollMessage(interval + scroller.getDuration()); + default: + break; + } + } + } + + /** + * get auto scroll time in milliseconds, default is {@link #DEFAULT_INTERVAL} + * + * @return the interval + */ + public long getInterval() { + return interval; + } + + /** + * set auto scroll time in milliseconds, default is {@link #DEFAULT_INTERVAL} + * + * @param interval the interval to set + */ + public void setInterval(long interval) { + this.interval = interval; + } + + /** + * get auto scroll direction + * + * @return {@link #LEFT} or {@link #RIGHT}, default is {@link #RIGHT} + */ + public int getDirection() { + return (direction == LEFT) ? LEFT : RIGHT; + } + + /** + * set auto scroll direction + * + * @param direction {@link #LEFT} or {@link #RIGHT}, default is {@link #RIGHT} + */ + public void setDirection(int direction) { + this.direction = direction; + } + + /** + * whether automatic cycle when auto scroll reaching the last or first item, default is true + * + * @return the isCycle + */ + public boolean isCycle() { + return isCycle; + } + + /** + * set whether automatic cycle when auto scroll reaching the last or first item, default is true + * + * @param isCycle the isCycle to set + */ + public void setCycle(boolean isCycle) { + this.isCycle = isCycle; + } + + /** + * whether stop auto scroll when touching, default is true + * + * @return the stopScrollWhenTouch + */ + public boolean isStopScrollWhenTouch() { + return stopScrollWhenTouch; + } + + public void setStopScrollWhenTouch(boolean stopScrollWhenTouch) { + this.stopScrollWhenTouch = stopScrollWhenTouch; + } + + /** + * get how to process when sliding at the last or first item + * + * @return the slideBorderMode {@link #SLIDE_BORDER_MODE_NONE}, {@link #SLIDE_BORDER_MODE_TO_PARENT}, + * {@link #SLIDE_BORDER_MODE_CYCLE}, default is {@link #SLIDE_BORDER_MODE_NONE} + */ + public int getSlideBorderMode() { + return slideBorderMode; + } + + /** + * set how to process when sliding at the last or first item + * + * @param slideBorderMode {@link #SLIDE_BORDER_MODE_NONE}, {@link #SLIDE_BORDER_MODE_TO_PARENT}, + * {@link #SLIDE_BORDER_MODE_CYCLE}, default is {@link #SLIDE_BORDER_MODE_NONE} + */ + public void setSlideBorderMode(int slideBorderMode) { + this.slideBorderMode = slideBorderMode; + } + + public boolean isBorderAnimation() { + return isBorderAnimation; + } + + public void setBorderAnimation(boolean isBorderAnimation) { + this.isBorderAnimation = isBorderAnimation; + } +} \ No newline at end of file diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/CustomViewPager/CustomDurationScroller.java b/widgets/src/main/java/mohammadaminha/com/widgets/CustomViewPager/CustomDurationScroller.java new file mode 100644 index 0000000..ab1e141 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/CustomViewPager/CustomDurationScroller.java @@ -0,0 +1,32 @@ +package mohammadaminha.com.widgets.CustomViewPager; + + +import android.content.Context; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +class CustomDurationScroller extends Scroller { + private double scrollFactor = 1; + + public CustomDurationScroller(Context context) { + super(context); + } + + public CustomDurationScroller(Context context, Interpolator interpolator) { + super(context, interpolator); + } + + // @SuppressLint("NewApi") + // public CustomDurationScroller(Context context, Interpolator interpolator, boolean flywheel){ + // super(context, interpolator, flywheel); + // } + + public void setScrollDurationFactor(double scrollFactor) { + this.scrollFactor = scrollFactor; + } + + @Override + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + super.startScroll(startX, startY, dx, dy, (int)(duration * scrollFactor)); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/AccessibleLinearLayout.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/AccessibleLinearLayout.java new file mode 100644 index 0000000..4839a9c --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/AccessibleLinearLayout.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Button; +import android.widget.LinearLayout; + +/** + * Fake Button class, used so TextViews can announce themselves as Buttons, for accessibility. + */ +public class AccessibleLinearLayout extends LinearLayout { + + public AccessibleLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(Button.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(Button.class.getName()); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/AccessibleTextView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/AccessibleTextView.java new file mode 100644 index 0000000..8229860 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/AccessibleTextView.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Button; + +import mohammadaminha.com.widgets.TextView; +import mohammadaminha.com.widgets.Util; + +/** + * Fake Button class, used so TextViews can announce themselves as Buttons, for accessibility. + */ +public class AccessibleTextView extends TextView { + + public AccessibleTextView(Context context, AttributeSet attrs) { + super(context, attrs); + setTypeface(context); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(Button.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(Button.class.getName()); + } + + + private void setTypeface(Context context) { + + setTypeface(Util.getTypeFace()); + } + + + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/HapticFeedbackController.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/HapticFeedbackController.java new file mode 100644 index 0000000..0ddecb2 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/HapticFeedbackController.java @@ -0,0 +1,74 @@ +package mohammadaminha.com.widgets.Date_Picker; + +import android.app.Service; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.SystemClock; +import android.os.Vibrator; +import android.provider.Settings; + +/** + * A simple utility class to handle haptic feedback. + */ +public class HapticFeedbackController { + private static final int VIBRATE_DELAY_MS = 125; + private static final int VIBRATE_LENGTH_MS = 5; + + private static boolean checkGlobalSetting(Context context) { + return Settings.System.getInt(context.getContentResolver(), + Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) == 1; + } + + private final Context mContext; + private final ContentObserver mContentObserver; + + private Vibrator mVibrator; + private boolean mIsGloballyEnabled; + private long mLastVibrate; + + public HapticFeedbackController(Context context) { + mContext = context; + mContentObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + mIsGloballyEnabled = checkGlobalSetting(mContext); + } + }; + } + + /** + * Call to setup the controller. + */ + public void start() { + mVibrator = (Vibrator) mContext.getSystemService(Service.VIBRATOR_SERVICE); + + // Setup a listener for changes in haptic feedback settings + mIsGloballyEnabled = checkGlobalSetting(mContext); + Uri uri = Settings.System.getUriFor(Settings.System.HAPTIC_FEEDBACK_ENABLED); + mContext.getContentResolver().registerContentObserver(uri, false, mContentObserver); + } + + /** + * Call this when you don't need the controller anymore. + */ + public void stop() { + mVibrator = null; + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + } + + /** + * Try to vibrate. To prevent this becoming a single continuous vibration, nothing will + * happen if we have vibrated very recently. + */ + public void tryVibrate() { + if (mVibrator != null && mIsGloballyEnabled) { + long now = SystemClock.uptimeMillis(); + // We want to try to vibrate each individual tick discretely. + if (now - mLastVibrate >= VIBRATE_DELAY_MS) { + mVibrator.vibrate(VIBRATE_LENGTH_MS); + mLastVibrate = now; + } + } + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/TypefaceHelper.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/TypefaceHelper.java new file mode 100644 index 0000000..f3581d5 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/TypefaceHelper.java @@ -0,0 +1,39 @@ +package mohammadaminha.com.widgets.Date_Picker; + + +import android.content.Context; +import android.graphics.Typeface; +import android.support.v4.util.SimpleArrayMap; + +/* + Each call to Typeface.createFromAsset will load a new instance of the typeface into memory, + and this memory is not consistently get garbage collected + http://code.google.com/p/android/issues/detail?id=9904 + (It states released but even on Lollipop you can see the typefaces accumulate even after + multiple GC passes) + You can detect this by running: + adb shell dumpsys meminfo com.your.packagenage + You will see output like: + Asset Allocations + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Regular.ttf: 123K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K +*/ +public class TypefaceHelper { + + private static final SimpleArrayMap cache = new SimpleArrayMap<>(); + + public static Typeface get(Context c, String name) { + synchronized (cache) { + if (!cache.containsKey(name)) { + Typeface t = Typeface.createFromAsset( + c.getAssets(), String.format("font/BYekan.ttf", name)); + cache.put(name, t); + return t; + } + return cache.get(name); + } + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/Utils.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/Utils.java new file mode 100644 index 0000000..8af0bb3 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/Utils.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker; + +import android.animation.Keyframe; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.annotation.SuppressLint; +import android.content.res.Resources; +import android.os.Build; +import android.util.TypedValue; +import android.view.View; + +import mohammadaminha.com.widgets.Date_Picker.utils.PersianCalendarUtils; + +/** + * Utility helper functions for time and date pickers. + */ +public class Utils { + + //public static final int MONDAY_BEFORE_JULIAN_EPOCH = Time.EPOCH_JULIAN_DAY - 3; + private static final int PULSE_ANIMATOR_DURATION = 544; + + // Alpha level for time picker selection. + public static final int SELECTED_ALPHA = 255; + public static final int SELECTED_ALPHA_THEME_DARK = 255; + // Alpha level for fully opaque. + public static final int FULL_ALPHA = 255; + + private static boolean isJellybeanOrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + } + + /** + * Try to speak the specified text, for accessibility. Only available on JB or later. + * @param text Text to announce. + */ + @SuppressLint("NewApi") + public static void tryAccessibilityAnnounce(View view, CharSequence text) { + if (isJellybeanOrLater() && view != null && text != null) { + view.announceForAccessibility(text); + } + } + + public static int getDaysInMonth(int month, int year) { + if (month < 6) { + return 31; + } else if (month < 11) { + return 30; + } else { + if (PersianCalendarUtils.isPersianLeapYear(year)) return 30; + else return 29; + } + } + + /** + * Render an animator to pulsate a view in place. + * @param labelToAnimate the view to pulsate. + * @return The animator object. Use .start() to begin. + */ + public static ObjectAnimator getPulseAnimator(View labelToAnimate, float decreaseRatio, + float increaseRatio) { + Keyframe k0 = Keyframe.ofFloat(0f, 1f); + Keyframe k1 = Keyframe.ofFloat(0.275f, decreaseRatio); + Keyframe k2 = Keyframe.ofFloat(0.69f, increaseRatio); + Keyframe k3 = Keyframe.ofFloat(1f, 1f); + + PropertyValuesHolder scaleX = PropertyValuesHolder.ofKeyframe("scaleX", k0, k1, k2, k3); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofKeyframe("scaleY", k0, k1, k2, k3); + ObjectAnimator pulseAnimator = + ObjectAnimator.ofPropertyValuesHolder(labelToAnimate, scaleX, scaleY); + pulseAnimator.setDuration(PULSE_ANIMATOR_DURATION); + + return pulseAnimator; + } + + /** + * Convert Dp to Pixel + */ + @SuppressWarnings("unused") + public static int dpToPx(float dp, Resources resources){ + float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.getDisplayMetrics()); + return (int) px; + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/AccessibleDateAnimator.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/AccessibleDateAnimator.java new file mode 100644 index 0000000..1dfe83d --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/AccessibleDateAnimator.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.date; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.widget.ViewAnimator; + +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.Date_Picker.utils.PersianCalendar; + +public class AccessibleDateAnimator extends ViewAnimator { + private long mDateMillis; + + public AccessibleDateAnimator(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setDateMillis(long dateMillis) { + mDateMillis = dateMillis; + } + + /** + * Announce the currently-selected date when launched. + */ + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + // Clear the event's current text so that only the current date will be spoken. + event.getText().clear(); + PersianCalendar mPersianCalendar = new PersianCalendar(); + mPersianCalendar.setTimeInMillis(mDateMillis); + String dateString = LanguageUtils.getPersianNumbers( + mPersianCalendar.getPersianMonthName() + " " + + mPersianCalendar.getPersianYear() + ); + event.getText().add(dateString); + return true; + } + return super.dispatchPopulateAccessibilityEvent(event); + } +} \ No newline at end of file diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/DatePickerController.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/DatePickerController.java new file mode 100644 index 0000000..91960b8 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/DatePickerController.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.date; + +import mohammadaminha.com.widgets.Date_Picker.utils.PersianCalendar; + +/** + * Controller class to communicate among the various components of the date picker dialog. + */ +public interface DatePickerController { + + void onYearSelected(int year); + + void onDayOfMonthSelected(int year, int month, int day); + + void registerOnDateChangedListener(DatePickerDialog.OnDateChangedListener listener); + + void unregisterOnDateChangedListener(DatePickerDialog.OnDateChangedListener listener); + + MonthAdapter.CalendarDay getSelectedDay(); + + boolean isThemeDark(); + + PersianCalendar[] getHighlightedDays(); + + PersianCalendar[] getSelectableDays(); + + int getFirstDayOfWeek(); + + int getMinYear(); + + int getMaxYear(); + + PersianCalendar getMinDate(); + + PersianCalendar getMaxDate(); + + void tryVibrate(); +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/DatePickerDialog.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/DatePickerDialog.java new file mode 100644 index 0000000..a2a0faf --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/DatePickerDialog.java @@ -0,0 +1,635 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.date; + +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.widget.LinearLayout; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashSet; + +import mohammadaminha.com.widgets.Button; +import mohammadaminha.com.widgets.Date_Picker.AccessibleTextView; +import mohammadaminha.com.widgets.Date_Picker.HapticFeedbackController; +import mohammadaminha.com.widgets.Date_Picker.TypefaceHelper; +import mohammadaminha.com.widgets.Date_Picker.Utils; +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.Date_Picker.utils.PersianCalendar; +import mohammadaminha.com.widgets.R; +import mohammadaminha.com.widgets.TextView; + +/** + * Dialog allowing users to select a date. + */ +public class DatePickerDialog extends DialogFragment implements + OnClickListener, DatePickerController { + + private static final String TAG = "DatePickerDialog"; + + private static final int UNINITIALIZED = -1; + private static final int MONTH_AND_DAY_VIEW = 0; + private static final int YEAR_VIEW = 1; + + private static final String KEY_SELECTED_YEAR = "year"; + private static final String KEY_SELECTED_MONTH = "month"; + private static final String KEY_SELECTED_DAY = "day"; + private static final String KEY_LIST_POSITION = "list_position"; + private static final String KEY_WEEK_START = "week_start"; + private static final String KEY_YEAR_START = "year_start"; + private static final String KEY_YEAR_END = "year_end"; + private static final String KEY_CURRENT_VIEW = "current_view"; + private static final String KEY_LIST_POSITION_OFFSET = "list_position_offset"; + private static final String KEY_MIN_DATE = "min_date"; + private static final String KEY_MAX_DATE = "max_date"; + private static final String KEY_HIGHLIGHTED_DAYS = "highlighted_days"; + private static final String KEY_SELECTABLE_DAYS = "selectable_days"; + private static final String KEY_THEME_DARK = "theme_dark"; + + private static final int DEFAULT_START_YEAR = 1250; + private static final int DEFAULT_END_YEAR = 1550; + + private static final int ANIMATION_DURATION = 300; + private static final int ANIMATION_DELAY = 500; + + private PersianCalendar mPersianCalendar = new PersianCalendar(); + private OnDateSetListener mCallBack; + private HashSet mListeners = new HashSet<>(); + private DialogInterface.OnCancelListener mOnCancelListener; + private DialogInterface.OnDismissListener mOnDismissListener; + + private AccessibleDateAnimator mAnimator; + + private TextView mDayOfWeekView; + private LinearLayout mMonthAndDayView; + private TextView mSelectedMonthTextView, mSelectedDayTextView; + private AccessibleTextView mYearView; + private DayPickerView mDayPickerView; + private YearPickerView mYearPickerView; + + private int mCurrentView = UNINITIALIZED; + + private int mWeekStart = PersianCalendar.SATURDAY; + private int mMinYear = DEFAULT_START_YEAR; + private int mMaxYear = DEFAULT_END_YEAR; + private PersianCalendar mMinDate; + private PersianCalendar mMaxDate; + private PersianCalendar[] highlightedDays; + private PersianCalendar[] selectableDays; + private boolean mThemeDark; + + private HapticFeedbackController mHapticFeedbackController; + + private boolean mDelayAnimation = true; + + // Accessibility strings. + private String mDayPickerDescription; + private String mSelectDay; + private String mYearPickerDescription; + private String mSelectYear; + + /** + * The callback used to indicate the user is done filling in the date. + */ + public interface OnDateSetListener { + + /** + * @param view The view associated with this listener. + * @param year The year that was set. + * @param monthOfYear The month that was set (0-11) for compatibility + * with {@link Calendar}. + * @param dayOfMonth The day of the month that was set. + */ + void onDateSet(DatePickerDialog view, int year, int monthOfYear, int dayOfMonth); + } + + /** + * The callback used to notify other date picker components of a change in selected date. + */ + public interface OnDateChangedListener { + + void onDateChanged(); + } + + + public DatePickerDialog() { + // Empty constructor required for dialog fragment. + } + + /** + * @param callBack How the parent is notified that the date is set. + * @param year The initial year of the dialog. + * @param monthOfYear The initial month of the dialog. + * @param dayOfMonth The initial day of the dialog. + */ + public static DatePickerDialog newInstance(OnDateSetListener callBack, int year, + int monthOfYear, + int dayOfMonth) { + DatePickerDialog ret = new DatePickerDialog(); + ret.initialize(callBack, year, monthOfYear, dayOfMonth); + return ret; + } + + private void initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) { + mCallBack = callBack; + mPersianCalendar.setPersianDate(year, monthOfYear, dayOfMonth); + mThemeDark = false; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Activity activity = getActivity(); + activity.getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + if (savedInstanceState != null) { + mPersianCalendar.setPersianDate( + savedInstanceState.getInt(KEY_SELECTED_YEAR), + savedInstanceState.getInt(KEY_SELECTED_MONTH), + savedInstanceState.getInt(KEY_SELECTED_DAY) + ); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(KEY_SELECTED_YEAR, mPersianCalendar.getPersianYear()); + outState.putInt(KEY_SELECTED_MONTH, mPersianCalendar.getPersianMonth()); + outState.putInt(KEY_SELECTED_DAY, mPersianCalendar.getPersianDay()); + outState.putInt(KEY_WEEK_START, mWeekStart); + outState.putInt(KEY_YEAR_START, mMinYear); + outState.putInt(KEY_YEAR_END, mMaxYear); + outState.putInt(KEY_CURRENT_VIEW, mCurrentView); + int listPosition = -1; + if (mCurrentView == MONTH_AND_DAY_VIEW) { + listPosition = mDayPickerView.getMostVisiblePosition(); + } else if (mCurrentView == YEAR_VIEW) { + listPosition = mYearPickerView.getFirstVisiblePosition(); + outState.putInt(KEY_LIST_POSITION_OFFSET, mYearPickerView.getFirstPositionOffset()); + } + outState.putInt(KEY_LIST_POSITION, listPosition); + outState.putSerializable(KEY_MIN_DATE, mMinDate); + outState.putSerializable(KEY_MAX_DATE, mMaxDate); + outState.putSerializable(KEY_HIGHLIGHTED_DAYS, highlightedDays); + outState.putSerializable(KEY_SELECTABLE_DAYS, selectableDays); + outState.putBoolean(KEY_THEME_DARK, mThemeDark); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Log.d(TAG, "onCreateView: "); + getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE); + + View view = inflater.inflate(R.layout.mdtp_date_picker_dialog, null); + + mDayOfWeekView = view.findViewById(R.id.date_picker_header); + mMonthAndDayView = view.findViewById(R.id.date_picker_month_and_day); + mMonthAndDayView.setOnClickListener(this); + mSelectedMonthTextView = view.findViewById(R.id.date_picker_month); + mSelectedDayTextView = view.findViewById(R.id.date_picker_day); + mYearView = view.findViewById(R.id.date_picker_year); + mYearView.setOnClickListener(this); + + int listPosition = -1; + int listPositionOffset = 0; + int currentView = MONTH_AND_DAY_VIEW; + if (savedInstanceState != null) { + mWeekStart = savedInstanceState.getInt(KEY_WEEK_START); + mMinYear = savedInstanceState.getInt(KEY_YEAR_START); + mMaxYear = savedInstanceState.getInt(KEY_YEAR_END); + currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW); + listPosition = savedInstanceState.getInt(KEY_LIST_POSITION); + listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET); + mMinDate = (PersianCalendar) savedInstanceState.getSerializable(KEY_MIN_DATE); + mMaxDate = (PersianCalendar) savedInstanceState.getSerializable(KEY_MAX_DATE); + highlightedDays = (PersianCalendar[]) savedInstanceState.getSerializable(KEY_HIGHLIGHTED_DAYS); + selectableDays = (PersianCalendar[]) savedInstanceState.getSerializable(KEY_SELECTABLE_DAYS); + mThemeDark = savedInstanceState.getBoolean(KEY_THEME_DARK); + } + + final Activity activity = getActivity(); + mDayPickerView = new SimpleDayPickerView(activity, this); + mYearPickerView = new YearPickerView(activity, this); + + Resources res = getResources(); + mDayPickerDescription = res.getString(R.string.mdtp_day_picker_description); + mSelectDay = res.getString(R.string.mdtp_select_day); + mYearPickerDescription = res.getString(R.string.mdtp_year_picker_description); + mSelectYear = res.getString(R.string.mdtp_select_year); + + int bgColorResource = mThemeDark ? R.color.mdtp_date_picker_view_animator_dark_theme : R.color.mdtp_date_picker_view_animator; + view.setBackgroundColor(activity.getResources().getColor(bgColorResource)); + + mAnimator = view.findViewById(R.id.animator); + mAnimator.addView(mDayPickerView); + mAnimator.addView(mYearPickerView); + mAnimator.setDateMillis(mPersianCalendar.getTimeInMillis()); + // TODO: Replace with animation decided upon by the design team. + Animation animation = new AlphaAnimation(0.0f, 1.0f); + animation.setDuration(ANIMATION_DURATION); + mAnimator.setInAnimation(animation); + // TODO: Replace with animation decided upon by the design team. + Animation animation2 = new AlphaAnimation(1.0f, 0.0f); + animation2.setDuration(ANIMATION_DURATION); + mAnimator.setOutAnimation(animation2); + + Button okButton = view.findViewById(R.id.ok); + okButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + tryVibrate(); + if (mCallBack != null) { + mCallBack.onDateSet(DatePickerDialog.this, mPersianCalendar.getPersianYear(), + mPersianCalendar.getPersianMonth(), mPersianCalendar.getPersianDay()); + } + dismiss(); + } + }); + okButton.setTypeface(TypefaceHelper.get(activity, "Roboto-Medium")); + + Button cancelButton = view.findViewById(R.id.cancel); + cancelButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + tryVibrate(); + getDialog().cancel(); + } + }); + cancelButton.setTypeface(TypefaceHelper.get(activity, "Roboto-Medium")); + cancelButton.setVisibility(isCancelable() ? View.VISIBLE : View.GONE); + + updateDisplay(false); + setCurrentView(currentView); + + if (listPosition != -1) { + if (currentView == MONTH_AND_DAY_VIEW) { + mDayPickerView.postSetSelection(listPosition); + } else if (currentView == YEAR_VIEW) { + mYearPickerView.postSetSelectionFromTop(listPosition, listPositionOffset); + } + } + + mHapticFeedbackController = new HapticFeedbackController(activity); + return view; + } + + @Override + public void onResume() { + super.onResume(); + mHapticFeedbackController.start(); + } + + @Override + public void onPause() { + super.onPause(); + mHapticFeedbackController.stop(); + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + if (mOnCancelListener != null) mOnCancelListener.onCancel(dialog); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (mOnDismissListener != null) mOnDismissListener.onDismiss(dialog); + } + + private void setCurrentView(final int viewIndex) { + + switch (viewIndex) { + case MONTH_AND_DAY_VIEW: + ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mMonthAndDayView, 0.9f, + 1.05f); + if (mDelayAnimation) { + pulseAnimator.setStartDelay(ANIMATION_DELAY); + mDelayAnimation = false; + } + mDayPickerView.onDateChanged(); + if (mCurrentView != viewIndex) { + mMonthAndDayView.setSelected(true); + mYearView.setSelected(false); + mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW); + mCurrentView = viewIndex; + } + pulseAnimator.start(); + + String dayString = LanguageUtils.getPersianNumbers(mPersianCalendar.getPersianLongDate()); + mAnimator.setContentDescription(mDayPickerDescription + ": " + dayString); + Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay); + break; + case YEAR_VIEW: + pulseAnimator = Utils.getPulseAnimator(mYearView, 0.85f, 1.1f); + if (mDelayAnimation) { + pulseAnimator.setStartDelay(ANIMATION_DELAY); + mDelayAnimation = false; + } + mYearPickerView.onDateChanged(); + if (mCurrentView != viewIndex) { + mMonthAndDayView.setSelected(false); + mYearView.setSelected(true); + mAnimator.setDisplayedChild(YEAR_VIEW); + mCurrentView = viewIndex; + } + pulseAnimator.start(); + + String yearString = LanguageUtils. + getPersianNumbers(String.valueOf(mPersianCalendar.getPersianYear())); + mAnimator.setContentDescription(mYearPickerDescription + ": " + yearString); + Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear); + break; + } + } + + private void updateDisplay(boolean announce) { + if (mDayOfWeekView != null) { + mDayOfWeekView.setText(mPersianCalendar.getPersianWeekDayName()); + } + + mSelectedMonthTextView.setText(LanguageUtils. + getPersianNumbers(mPersianCalendar.getPersianMonthName())); + mSelectedDayTextView.setText(LanguageUtils. + getPersianNumbers(String.valueOf(mPersianCalendar.getPersianDay()))); + mYearView.setText(LanguageUtils. + getPersianNumbers(String.valueOf(mPersianCalendar.getPersianYear()))); + + // Accessibility. + long millis = mPersianCalendar.getTimeInMillis(); + mAnimator.setDateMillis(millis); + String monthAndDayText = LanguageUtils.getPersianNumbers( + mPersianCalendar.getPersianMonthName() + " " + + mPersianCalendar.getPersianDay() + ); + mMonthAndDayView.setContentDescription(monthAndDayText); + + if (announce) { + String fullDateText = LanguageUtils. + getPersianNumbers(mPersianCalendar.getPersianLongDate()); + Utils.tryAccessibilityAnnounce(mAnimator, fullDateText); + } + } + + /** + * Set whether the dark theme should be used + * + * @param themeDark true if the dark theme should be used, false if the default theme should be used + */ + public void setThemeDark(boolean themeDark) { + mThemeDark = themeDark; + } + + /** + * Returns true when the dark theme should be used + * + * @return true if the dark theme should be used, false if the default theme should be used + */ + @Override + public boolean isThemeDark() { + return mThemeDark; + } + + @SuppressWarnings("unused") + public void setFirstDayOfWeek(int startOfWeek) { + if (startOfWeek < Calendar.SUNDAY || startOfWeek > Calendar.SATURDAY) { + throw new IllegalArgumentException("Value must be between Calendar.SUNDAY and " + + "Calendar.SATURDAY"); + } + mWeekStart = startOfWeek; + if (mDayPickerView != null) { + mDayPickerView.onChange(); + } + } + + @SuppressWarnings("unused") + public void setYearRange(int startYear, int endYear) { + if (endYear < startYear) { + throw new IllegalArgumentException("Year end must be larger than or equal to year start"); + } + + mMinYear = startYear; + mMaxYear = endYear; + if (mDayPickerView != null) { + mDayPickerView.onChange(); + } + } + + /** + * Sets the minimal date supported by this DatePicker. Dates before (but not including) the + * specified date will be disallowed from being selected. + * + * @param calendar a Calendar object set to the year, month, day desired as the mindate. + */ + @SuppressWarnings("unused") + public void setMinDate(PersianCalendar calendar) { + mMinDate = calendar; + + if (mDayPickerView != null) { + mDayPickerView.onChange(); + } + } + + /** + * @return The minimal date supported by this DatePicker. Null if it has not been set. + */ + @Override + public PersianCalendar getMinDate() { + return mMinDate; + } + + /** + * Sets the minimal date supported by this DatePicker. Dates after (but not including) the + * specified date will be disallowed from being selected. + * + * @param calendar a Calendar object set to the year, month, day desired as the maxdate. + */ + @SuppressWarnings("unused") + public void setMaxDate(PersianCalendar calendar) { + mMaxDate = calendar; + + if (mDayPickerView != null) { + mDayPickerView.onChange(); + } + } + + /** + * @return The maximal date supported by this DatePicker. Null if it has not been set. + */ + @Override + public PersianCalendar getMaxDate() { + return mMaxDate; + } + + /** + * Sets an array of dates which should be highlighted when the picker is drawn + * + * @param highlightedDays an Array of Calendar objects containing the dates to be highlighted + */ + @SuppressWarnings("unused") + public void setHighlightedDays(PersianCalendar[] highlightedDays) { + // Sort the array to optimize searching over it later on + Arrays.sort(highlightedDays); + this.highlightedDays = highlightedDays; + } + + /** + * @return The list of dates, as Calendar Objects, which should be highlighted. null is no dates should be highlighted + */ + @Override + public PersianCalendar[] getHighlightedDays() { + return highlightedDays; + } + + /** + * Set's a list of days which are the only valid selections. + * Setting this value will take precedence over using setMinDate() and setMaxDate() + * + * @param selectableDays an Array of Calendar Objects containing the selectable dates + */ + @SuppressWarnings("unused") + public void setSelectableDays(PersianCalendar[] selectableDays) { + // Sort the array to optimize searching over it later on + Arrays.sort(selectableDays); + this.selectableDays = selectableDays; + } + + /** + * @return an Array of Calendar objects containing the list with selectable items. null if no restriction is set + */ + @Override + public PersianCalendar[] getSelectableDays() { + return selectableDays; + } + + @SuppressWarnings("unused") + public void setOnDateSetListener(OnDateSetListener listener) { + mCallBack = listener; + } + + @SuppressWarnings("unused") + public void setOnCancelListener(DialogInterface.OnCancelListener onCancelListener) { + mOnCancelListener = onCancelListener; + } + + @SuppressWarnings("unused") + public void setOnDismissListener(DialogInterface.OnDismissListener onDismissListener) { + mOnDismissListener = onDismissListener; + } + + // If the newly selected month / year does not contain the currently selected day number, + // change the selected day number to the last day of the selected month or year. + // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30 + // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013 + private void adjustDayInMonthIfNeeded(int month, int year) { +// int day = mPersianCalendar.getPersianDay(); +// int daysInMonth = Utils.getDaysInMonth(month, year); +// if (day > daysInMonth) { +// mPersianCalendar.setPersianDate(Persian); +// } TODO + } + + @Override + public void onClick(View v) { + tryVibrate(); + if (v.getId() == R.id.date_picker_year) { + setCurrentView(YEAR_VIEW); + } else if (v.getId() == R.id.date_picker_month_and_day) { + setCurrentView(MONTH_AND_DAY_VIEW); + } + } + + @Override + public void onYearSelected(int year) { + adjustDayInMonthIfNeeded(mPersianCalendar.getPersianMonth(), year); + mPersianCalendar.setPersianDate(year, mPersianCalendar.getPersianMonth(), + mPersianCalendar.getPersianDay()); + updatePickers(); + setCurrentView(MONTH_AND_DAY_VIEW); + updateDisplay(true); + } + + @Override + public void onDayOfMonthSelected(int year, int month, int day) { + mPersianCalendar.setPersianDate(year, month, day); + updatePickers(); + updateDisplay(true); + } + + private void updatePickers() { + for (OnDateChangedListener listener : mListeners) listener.onDateChanged(); + } + + + @Override + public MonthAdapter.CalendarDay getSelectedDay() { + return new MonthAdapter.CalendarDay(mPersianCalendar); + } + + @Override + public int getMinYear() { + if (selectableDays != null) return selectableDays[0].getPersianYear(); + // Ensure no years can be selected outside of the given minimum date + return mMinDate != null && mMinDate.getPersianYear() > mMinYear ? mMinDate.getPersianYear() : mMinYear; + } + + @Override + public int getMaxYear() { + if (selectableDays != null) + return selectableDays[selectableDays.length - 1].getPersianYear(); + // Ensure no years can be selected outside of the given maximum date + return mMaxDate != null && mMaxDate.getPersianYear() < mMaxYear ? mMaxDate.getPersianYear() : mMaxYear; + } + + @Override + public int getFirstDayOfWeek() { + return mWeekStart; + } + + @Override + public void registerOnDateChangedListener(OnDateChangedListener listener) { + mListeners.add(listener); + } + + @Override + public void unregisterOnDateChangedListener(OnDateChangedListener listener) { + mListeners.remove(listener); + } + + @Override + public void tryVibrate() { + mHapticFeedbackController.tryVibrate(); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/DayPickerView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/DayPickerView.java new file mode 100644 index 0000000..9809175 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/DayPickerView.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.date; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.ListView; + +import mohammadaminha.com.widgets.Date_Picker.Utils; +import mohammadaminha.com.widgets.Date_Picker.date.DatePickerDialog.OnDateChangedListener; +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.Date_Picker.utils.PersianCalendar; + +/** + * This displays a list of months in a calendar format with selectable days. + */ +public abstract class DayPickerView extends ListView implements OnScrollListener, + OnDateChangedListener { + + private static final String TAG = "MonthFragment"; + + // Affects when the month selection will change while scrolling up + protected static final int SCROLL_HYST_WEEKS = 2; + // How long the GoTo fling animation should last + private static final int GOTO_SCROLL_DURATION = 250; + // How long to wait after receiving an onScrollStateChanged notification + // before acting on it + private static final int SCROLL_CHANGE_DELAY = 40; + // The number of days to display in each week + public static final int DAYS_PER_WEEK = 7; + private static final int LIST_TOP_OFFSET = -1; // so that the top line will be + // under the separator + // You can override these numbers to get a different appearance + protected int mNumWeeks = 6; + protected boolean mShowWeekNumber = false; + protected int mDaysPerWeek = 7; +// + // These affect the scroll speed and feel +private float mFriction = 1.0f; + + private Context mContext; + private Handler mHandler; + + // highlighted time + private MonthAdapter.CalendarDay mSelectedDay = new MonthAdapter.CalendarDay(); + private MonthAdapter mAdapter; + + private MonthAdapter.CalendarDay mTempDay = new MonthAdapter.CalendarDay(); + + // When the week starts; numbered like Time. (e.g. SUNDAY=0). + protected int mFirstDayOfWeek; + // The last name announced by accessibility + protected CharSequence mPrevMonthName; + // which month should be displayed/highlighted [0-11] + private int mCurrentMonthDisplayed; + // used for tracking during a scroll + private long mPreviousScrollPosition; + // used for tracking what state listview is in + private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; + // used for tracking what state listview is in + private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + private DatePickerController mController; + private boolean mPerformingScroll; + + public DayPickerView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + DayPickerView(Context context, DatePickerController controller) { + super(context); + init(context); + setController(controller); + } + + private void setController(DatePickerController controller) { + mController = controller; + mController.registerOnDateChangedListener(this); + refreshAdapter(); + onDateChanged(); + } + + private void init(Context context) { + mHandler = new Handler(); + setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + setDrawSelectorOnTop(false); + + mContext = context; + setUpListView(); + } + + public void onChange() { + refreshAdapter(); + } + + /** + * Creates a new adapter if necessary and sets up its parameters. Override + * this method to provide a custom adapter. + */ + private void refreshAdapter() { + if (mAdapter == null) { + mAdapter = createMonthAdapter(getContext(), mController); + } else { + mAdapter.setSelectedDay(mSelectedDay); + } + // refresh the view with the new parameters + setAdapter(mAdapter); + } + + protected abstract MonthAdapter createMonthAdapter(Context context, + DatePickerController controller); + + /* + * Sets all the required fields for the list view. Override this method to + * set a different list view behavior. + */ + private void setUpListView() { + // Transparent background on scroll + setCacheColorHint(0); + // No dividers + setDivider(null); + // Items are clickable + setItemsCanFocus(true); + // The thumb gets in the way, so disable it + setFastScrollEnabled(false); + setVerticalScrollBarEnabled(false); + setOnScrollListener(this); + setFadingEdgeLength(0); + // Make the scrolling behavior nicer + setFriction(ViewConfiguration.getScrollFriction() * mFriction); + } + + /** + * This moves to the specified time in the view. If the time is not already + * in range it will move the list so that the first of the month containing + * the time is at the top of the view. If the new time is already in view + * the list will not be scrolled unless forceScroll is true. This time may + * optionally be highlighted as selected as well. + * + * @param day The day to move to + * @param animate Whether to scroll to the given time or just redraw at the + * new location + * @param setSelected Whether to set the given time as selected + * @param forceScroll Whether to recenter even if the time is already + * visible + * @return Whether or not the view animated to the new location + */ + private void goTo(MonthAdapter.CalendarDay day, boolean animate, boolean setSelected, boolean forceScroll) { + + // Set the selected day + if (setSelected) { + mSelectedDay.set(day); + } + + mTempDay.set(day); + final int position = (day.year - mController.getMinYear()) + * MonthAdapter.MONTHS_IN_YEAR + day.month; + + View child; + int i = 0; + int top ; + // Find a child that's completely in the view + do { + child = getChildAt(i++); + if (child == null) { + break; + } + top = child.getTop(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "child at " + (i - 1) + " has top " + top); + } + } while (top < 0); + + // Compute the first and last position visible + int selectedPosition; + if (child != null) { + selectedPosition = getPositionForView(child); + } else { + selectedPosition = 0; + } + + if (setSelected) { + mAdapter.setSelectedDay(mSelectedDay); + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "GoTo position " + position); + } + // Check if the selected day is now outside of our visible range + // and if so scroll to the month that contains it + if (position != selectedPosition || forceScroll) { + setMonthDisplayed(mTempDay); + mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; + if (animate) { + smoothScrollToPositionFromTop( + position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION); + } else { + postSetSelection(position); + } + } else if (setSelected) { + setMonthDisplayed(mSelectedDay); + } + } + + public void postSetSelection(final int position) { + clearFocus(); + post(new Runnable() { + + @Override + public void run() { + setSelection(position); + } + }); + onScrollStateChanged(this, OnScrollListener.SCROLL_STATE_IDLE); + } + + /** + * Updates the title and selected month if the view has moved to a new + * month. + */ + @Override + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + MonthView child = (MonthView) view.getChildAt(0); + if (child == null) { + return; + } + + // Figure out where we are + mPreviousScrollPosition = (long) (view.getFirstVisiblePosition() * child.getHeight() - child.getBottom()); + mPreviousScrollState = mCurrentScrollState; + } + + /** + * Sets the month displayed at the top of this view based on time. Override + * to add custom events when the title is changed. + */ + private void setMonthDisplayed(MonthAdapter.CalendarDay date) { + mCurrentMonthDisplayed = date.month; + invalidateViews(); + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + // use a post to prevent re-entering onScrollStateChanged before it + // exits + mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); + } + + private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); + + protected class ScrollStateRunnable implements Runnable { + private int mNewState; + + /** + * Sets up the runnable with a short delay in case the scroll state + * immediately changes again. + * + * @param view The list view that changed state + * @param scrollState The new state it changed to + */ + public void doScrollStateChange(AbsListView view, int scrollState) { + mHandler.removeCallbacks(this); + mNewState = scrollState; + mHandler.postDelayed(this, SCROLL_CHANGE_DELAY); + } + + @Override + public void run() { + mCurrentScrollState = mNewState; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, + "new scroll state: " + mNewState + " old state: " + mPreviousScrollState); + } + // Fix the position after a scroll or a fling ends + if (mNewState == OnScrollListener.SCROLL_STATE_IDLE + && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE + && mPreviousScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + mPreviousScrollState = mNewState; + int i = 0; + View child = getChildAt(i); + while (child != null && child.getBottom() <= 0) { + child = getChildAt(++i); + } + if (child == null) { + // The view is no longer visible, just return + return; + } + int firstPosition = getFirstVisiblePosition(); + int lastPosition = getLastVisiblePosition(); + boolean scroll = firstPosition != 0 && lastPosition != getCount() - 1; + final int top = child.getTop(); + final int bottom = child.getBottom(); + final int midpoint = getHeight() / 2; + if (scroll && top < LIST_TOP_OFFSET) { + if (bottom > midpoint) { + smoothScrollBy(top, GOTO_SCROLL_DURATION); + } else { + smoothScrollBy(bottom, GOTO_SCROLL_DURATION); + } + } + } else { + mPreviousScrollState = mNewState; + } + } + } + + /** + * Gets the position of the view that is most prominently displayed within the list view. + */ + public int getMostVisiblePosition() { + final int firstPosition = getFirstVisiblePosition(); + final int height = getHeight(); + + int maxDisplayedHeight = 0; + int mostVisibleIndex = 0; + int i=0; + int bottom = 0; + while (bottom < height) { + View child = getChildAt(i); + if (child == null) { + break; + } + bottom = child.getBottom(); + int displayedHeight = Math.min(bottom, height) - Math.max(0, child.getTop()); + if (displayedHeight > maxDisplayedHeight) { + mostVisibleIndex = i; + maxDisplayedHeight = displayedHeight; + } + i++; + } + return firstPosition + mostVisibleIndex; + } + + @Override + public void onDateChanged() { + goTo(mController.getSelectedDay(), false, true, true); + } + + /** + * Attempts to return the date that has accessibility focus. + * + * @return The date that has accessibility focus, or {@code null} if no date + * has focus. + */ + private MonthAdapter.CalendarDay findAccessibilityFocus() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child instanceof MonthView) { + final MonthAdapter.CalendarDay focus = ((MonthView) child).getAccessibilityFocus(); + if (focus != null) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { + // Clear focus to avoid ListView bug in Jelly Bean MR1. + ((MonthView) child).clearAccessibilityFocus(); + } + return focus; + } + } + } + + return null; + } + + /** + * Attempts to restore accessibility focus to a given date. No-op if + * {@code day} is {@code null}. + * + * @param day The date that should receive accessibility focus + * @return {@code true} if focus was restored + */ + private void restoreAccessibilityFocus(MonthAdapter.CalendarDay day) { + if (day == null) { + return; + } + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child instanceof MonthView) { + if (((MonthView) child).restoreAccessibilityFocus(day)) { + return; + } + } + } + + } + + @Override + protected void layoutChildren() { + final MonthAdapter.CalendarDay focusedDay = findAccessibilityFocus(); + super.layoutChildren(); + if (mPerformingScroll) { + mPerformingScroll = false; + } else { + restoreAccessibilityFocus(focusedDay); + } + } + + @Override + public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setItemCount(-1); + } + + private static String getMonthAndYearString(MonthAdapter.CalendarDay day) { + PersianCalendar mPersianCalendar = new PersianCalendar(); + mPersianCalendar.setPersianDate(day.year, day.month, day.day); + + String sbuf = ""; + sbuf += mPersianCalendar.getPersianMonthName(); + sbuf += " "; + sbuf += mPersianCalendar.getPersianYear(); + return sbuf; + } + + /** + * Necessary for accessibility, to ensure we support "scrolling" forward and backward + * in the month list. + */ + @Override + @SuppressWarnings("deprecation") + public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + if(Build.VERSION.SDK_INT >= 21) { + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); + } + else { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + + /** + * When scroll forward/backward events are received, announce the newly scrolled-to month. + */ + @SuppressLint("NewApi") + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (action != AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && + action != AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + return super.performAccessibilityAction(action, arguments); + } + + // Figure out what month is showing. + int firstVisiblePosition = getFirstVisiblePosition(); + int month = firstVisiblePosition % 12; + int year = firstVisiblePosition / 12 + mController.getMinYear(); + MonthAdapter.CalendarDay day = new MonthAdapter.CalendarDay(year, month, 1); + + // Scroll either forward or backward one month. + if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { + day.month++; + if (day.month == 12) { + day.month = 0; + day.year++; + } + } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + View firstVisibleView = getChildAt(0); + // If the view is fully visible, jump one month back. Otherwise, we'll just jump + // to the first day of first visible month. + if (firstVisibleView != null && firstVisibleView.getTop() >= -1) { + // There's an off-by-one somewhere, so the top of the first visible item will + // actually be -1 when it's at the exact top. + day.month--; + if (day.month == -1) { + day.month = 11; + day.year--; + } + } + } + + // Go to that month. + Utils.tryAccessibilityAnnounce(this, + LanguageUtils.getPersianNumbers(getMonthAndYearString(day))); + goTo(day, true, false, true); + mPerformingScroll = true; + return true; + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/MonthAdapter.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/MonthAdapter.java new file mode 100644 index 0000000..d464a8e --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/MonthAdapter.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.date; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView.LayoutParams; +import android.widget.BaseAdapter; + +import java.util.HashMap; + +import mohammadaminha.com.widgets.Date_Picker.date.MonthView.OnDayClickListener; +import mohammadaminha.com.widgets.Date_Picker.utils.PersianCalendar; + +/** + * An adapter for a list of {@link MonthView} items. + */ +public abstract class MonthAdapter extends BaseAdapter implements OnDayClickListener { + + private static final String TAG = "SimpleMonthAdapter"; + + private Context mContext; + final DatePickerController mController; + + private CalendarDay mSelectedDay; + + protected static int WEEK_7_OVERHANG_HEIGHT = 7; + static final int MONTHS_IN_YEAR = 12; + + /** + * A convenience class to represent a specific date. + */ + public static class CalendarDay { + private PersianCalendar mPersianCalendar; + int year; + int month; + int day; + + public CalendarDay() { + setTime(System.currentTimeMillis()); + } + + public CalendarDay(long timeInMillis) { + setTime(timeInMillis); + } + + public CalendarDay(PersianCalendar calendar) { + year = calendar.getPersianYear(); + month = calendar.getPersianMonth(); + day = calendar.getPersianDay(); + } + + public CalendarDay(int year, int month, int day) { + setDay(year, month, day); + } + + public void set(CalendarDay date) { + year = date.year; + month = date.month; + day = date.day; + } + + public void setDay(int year, int month, int day) { + this.year = year; + this.month = month; + this.day = day; + } + + private void setTime(long timeInMillis) { + if (mPersianCalendar == null) { + mPersianCalendar = new PersianCalendar(); + } + mPersianCalendar.setTimeInMillis(timeInMillis); + month = mPersianCalendar.getPersianMonth(); + year = mPersianCalendar.getPersianYear(); + day = mPersianCalendar.getPersianDay(); + } + + public int getYear() { + return year; + } + + public int getMonth() { + return month; + } + + public int getDay() { + return day; + } + } + + MonthAdapter(Context context, + DatePickerController controller) { + mContext = context; + mController = controller; + init(); + setSelectedDay(mController.getSelectedDay()); + } + + /** + * Updates the selected day and related parameters. + * + * @param day The day to highlight + */ + public void setSelectedDay(CalendarDay day) { + mSelectedDay = day; + notifyDataSetChanged(); + } + + public CalendarDay getSelectedDay() { + return mSelectedDay; + } + + /** + * Set up the gesture detector and selected time + */ + private void init() { + mSelectedDay = new CalendarDay(System.currentTimeMillis()); + } + + @Override + public int getCount() { + return ((mController.getMaxYear() - mController.getMinYear()) + 1) * MONTHS_IN_YEAR; + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @SuppressLint("NewApi") + @SuppressWarnings("unchecked") + @Override + public View getView(int position, View convertView, ViewGroup parent) { + MonthView v; + HashMap drawingParams = null; + if (convertView != null) { + v = (MonthView) convertView; + // We store the drawing parameters in the view so it can be recycled + drawingParams = (HashMap) v.getTag(); + } else { + v = createMonthView(mContext); + // Set up the new view + LayoutParams params = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + v.setLayoutParams(params); + v.setClickable(true); + v.setOnDayClickListener(this); + } + if (drawingParams == null) { + drawingParams = new HashMap<>(); + } + drawingParams.clear(); + + final int month = position % MONTHS_IN_YEAR; + final int year = position / MONTHS_IN_YEAR + mController.getMinYear(); + + int selectedDay = -1; + if (isSelectedDayInMonth(year, month)) { + selectedDay = mSelectedDay.day; + } + + // Invokes requestLayout() to ensure that the recycled view is set with the appropriate + // height/number of weeks before being displayed. + v.reuse(); + + drawingParams.put(MonthView.VIEW_PARAMS_SELECTED_DAY, selectedDay); + drawingParams.put(MonthView.VIEW_PARAMS_YEAR, year); + drawingParams.put(MonthView.VIEW_PARAMS_MONTH, month); + drawingParams.put(MonthView.VIEW_PARAMS_WEEK_START, mController.getFirstDayOfWeek()); + v.setMonthParams(drawingParams); + v.invalidate(); + return v; + } + + protected abstract MonthView createMonthView(Context context); + + private boolean isSelectedDayInMonth(int year, int month) { + return mSelectedDay.year == year && mSelectedDay.month == month; + } + + + @Override + public void onDayClick(MonthView view, CalendarDay day) { + if (day != null) { + onDayTapped(day); + } + } + + /** + * Maintains the same hour/min/sec but moves the day to the tapped day. + * + * @param day The day that was tapped + */ + private void onDayTapped(CalendarDay day) { + mController.tryVibrate(); + mController.onDayOfMonthSelected(day.year, day.month, day.day); + setSelectedDay(day); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/MonthView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/MonthView.java new file mode 100644 index 0000000..dcbaaf3 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/MonthView.java @@ -0,0 +1,828 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.date; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.widget.ExploreByTouchHelper; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import java.security.InvalidParameterException; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; + +import mohammadaminha.com.widgets.Date_Picker.TypefaceHelper; +import mohammadaminha.com.widgets.Date_Picker.Utils; +import mohammadaminha.com.widgets.Date_Picker.date.MonthAdapter.CalendarDay; +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.Date_Picker.utils.PersianCalendar; +import mohammadaminha.com.widgets.R; +import mohammadaminha.com.widgets.Util; + +/** + * A calendar-like view displaying a specified month and the appropriate selectable day numbers + * within the specified month. + */ +public abstract class MonthView extends View { + private static final String TAG = "MonthView"; + + /** + * This sets the height of this week in pixels + */ + private static final String VIEW_PARAMS_HEIGHT = "height"; + /** + * This specifies the position (or weeks since the epoch) of this week. + */ + public static final String VIEW_PARAMS_MONTH = "month"; + /** + * This specifies the position (or weeks since the epoch) of this week. + */ + public static final String VIEW_PARAMS_YEAR = "year"; + /** + * This sets one of the days in this view as selected {@link Calendar#SUNDAY} + * through {@link Calendar#SATURDAY}. + */ + public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day"; + /** + * Which day the week should start on. {@link Calendar#SUNDAY} through + * {@link Calendar#SATURDAY}. + */ + public static final String VIEW_PARAMS_WEEK_START = "week_start"; + /** + * How many days to display at a time. Days will be displayed starting with + * {@link #mWeekStart}. + */ + public static final String VIEW_PARAMS_NUM_DAYS = "num_days"; + /** + * Which month is currently in focus, as defined by {@link Calendar#MONTH} + * [0-11]. + */ + public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month"; + /** + * If this month should display week numbers. false if 0, true otherwise. + */ + public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num"; + + private static final int DEFAULT_HEIGHT = 32; + private static final int MIN_HEIGHT = 10; + private static final int DEFAULT_SELECTED_DAY = -1; + private static final int DEFAULT_WEEK_START = Calendar.SATURDAY; + private static final int DEFAULT_NUM_DAYS = 7; + protected static final int DEFAULT_SHOW_WK_NUM = 0; + protected static final int DEFAULT_FOCUS_MONTH = -1; + private static final int DEFAULT_NUM_ROWS = 6; + private static final int MAX_NUM_ROWS = 6; + + private static final int SELECTED_CIRCLE_ALPHA = 255; + + private static final int DAY_SEPARATOR_WIDTH = 1; + static int MINI_DAY_NUMBER_TEXT_SIZE; + private static int MONTH_LABEL_TEXT_SIZE; + private static int MONTH_DAY_LABEL_TEXT_SIZE; + private static int MONTH_HEADER_SIZE; + static int DAY_SELECTED_CIRCLE_SIZE; + + // used for scaling to the device density + protected static float mScale = 0; + + private DatePickerController mController; + + // affects the padding on the sides of this view + private int mEdgePadding = 0; + + + Paint mMonthNumPaint; + private Paint mMonthTitlePaint; + Paint mSelectedCirclePaint; + private Paint mMonthDayLabelPaint; + + private StringBuilder mStringBuilder; + + // The Julian day of the first day displayed by this item + protected int mFirstJulianDay = -1; + // The month of the first day in this week + protected int mFirstMonth = -1; + // The month of the last day in this week + protected int mLastMonth = -1; + + private int mMonth; + + private int mYear; + // Quick reference to the width of this view, matches parent + private int mWidth; + // The height this view should draw at in pixels, set by height param + private int mRowHeight = DEFAULT_HEIGHT; + // If this view contains the today + boolean mHasToday = false; + // Which day is selected [0-6] or -1 if no day is selected + int mSelectedDay = -1; + // Which day is today [0-6] or -1 if no day is today + int mToday = DEFAULT_SELECTED_DAY; + // Which day of the week to start on [0-6] + private int mWeekStart = DEFAULT_WEEK_START; + // How many days to display + private int mNumDays = DEFAULT_NUM_DAYS; + // The number of days + a spot for week number if it is displayed + private int mNumCells = mNumDays; + // The left edge of the selected day + protected int mSelectedLeft = -1; + // The right edge of the selected day + protected int mSelectedRight = -1; + + private PersianCalendar mPersianCalendar; + private PersianCalendar mDayLabelCalendar; + private MonthViewTouchHelper mTouchHelper; + + private int mNumRows = DEFAULT_NUM_ROWS; + + // Optional listener for handling day click actions + private OnDayClickListener mOnDayClickListener; + + // Whether to prevent setting the accessibility delegate + private boolean mLockAccessibilityDelegate; + + final int mDayTextColor; + final int mSelectedDayTextColor; + private int mMonthDayTextColor; + final int mTodayNumberColor; + final int mHighlightedDayTextColor; + final int mDisabledDayTextColor; + private int mMonthTitleColor; + + public MonthView(Context context) { + this(context, null, null); + } + + MonthView(Context context, AttributeSet attr, DatePickerController controller) { + super(context, attr); + mController = controller; + Resources res = context.getResources(); + + mDayLabelCalendar = new PersianCalendar(); + mPersianCalendar = new PersianCalendar(); + + boolean darkTheme = mController != null && mController.isThemeDark(); + if (darkTheme) { + mDayTextColor = res.getColor(R.color.mdtp_date_picker_text_normal_dark_theme); + mMonthDayTextColor = res.getColor(R.color.mdtp_date_picker_month_day_dark_theme); + mDisabledDayTextColor = res.getColor(R.color.mdtp_date_picker_text_disabled_dark_theme); + mHighlightedDayTextColor = res.getColor(R.color.mdtp_date_picker_text_highlighted_dark_theme); + } else { + mDayTextColor = res.getColor(R.color.mdtp_date_picker_text_normal); + mMonthDayTextColor = res.getColor(R.color.mdtp_date_picker_month_day); + mDisabledDayTextColor = res.getColor(R.color.mdtp_date_picker_text_disabled); + mHighlightedDayTextColor = res.getColor(R.color.mdtp_date_picker_text_highlighted); + } + mSelectedDayTextColor = res.getColor(R.color.mdtp_white); + mTodayNumberColor = res.getColor(R.color.mdtp_accent_color); + mMonthTitleColor = res.getColor(R.color.mdtp_white); + + mStringBuilder = new StringBuilder(50); + + MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_day_number_size); + MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_month_label_size); + MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_month_day_label_text_size); + MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.mdtp_month_list_item_header_height); + DAY_SELECTED_CIRCLE_SIZE = res + .getDimensionPixelSize(R.dimen.mdtp_day_number_select_circle_radius); + + mRowHeight = (res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height) + - getMonthHeaderSize()) / MAX_NUM_ROWS; + + // Set up accessibility components. + mTouchHelper = getMonthViewTouchHelper(); + ViewCompat.setAccessibilityDelegate(this, mTouchHelper); + ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + mLockAccessibilityDelegate = true; + + // Sets up any standard paints that will be used + initView(); + } + + public void setDatePickerController(DatePickerController controller) { + mController = controller; + } + + private MonthViewTouchHelper getMonthViewTouchHelper() { + return new MonthViewTouchHelper(this); + } + + @Override + public void setAccessibilityDelegate(AccessibilityDelegate delegate) { + // Workaround for a JB MR1 issue where accessibility delegates on + // top-level ListView items are overwritten. + if (!mLockAccessibilityDelegate) { + super.setAccessibilityDelegate(delegate); + } + } + + public void setOnDayClickListener(OnDayClickListener listener) { + mOnDayClickListener = listener; + } + + @Override + public boolean dispatchHoverEvent(@NonNull MotionEvent event) { + // First right-of-refusal goes the touch exploration helper. + return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event); + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + final int day = getDayFromLocation(event.getX(), event.getY()); + if (day >= 0) { + onDayClick(day); + } + break; + } + return true; + } + + /** + * Sets up the text and style properties for painting. Override this if you + * want to use a different paint. + */ + private void initView() { + mMonthTitlePaint = new Paint(); + mMonthTitlePaint.setFakeBoldText(true); + mMonthTitlePaint.setAntiAlias(true); + mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE); + + mMonthTitlePaint.setTypeface(Util.getTypeFace()); + mMonthTitlePaint.setColor(mDayTextColor); + mMonthTitlePaint.setTextAlign(Align.CENTER); + mMonthTitlePaint.setStyle(Style.FILL); + + mSelectedCirclePaint = new Paint(); + mSelectedCirclePaint.setFakeBoldText(true); + mSelectedCirclePaint.setAntiAlias(true); + mSelectedCirclePaint.setColor(mTodayNumberColor); + mSelectedCirclePaint.setTextAlign(Align.CENTER); + mSelectedCirclePaint.setStyle(Style.FILL); + mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA); + + mMonthDayLabelPaint = new Paint(); + mMonthDayLabelPaint.setAntiAlias(true); + mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE); + mMonthDayLabelPaint.setColor(mMonthDayTextColor); + mMonthDayLabelPaint.setTypeface(TypefaceHelper.get(getContext(), "Roboto-Medium")); + mMonthDayLabelPaint.setStyle(Style.FILL); + mMonthDayLabelPaint.setTextAlign(Align.CENTER); + mMonthDayLabelPaint.setFakeBoldText(true); + + mMonthNumPaint = new Paint(); + mMonthNumPaint.setAntiAlias(true); + mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE); + mMonthNumPaint.setStyle(Style.FILL); + mMonthNumPaint.setTextAlign(Align.CENTER); + mMonthNumPaint.setFakeBoldText(false); + } + + @Override + protected void onDraw(Canvas canvas) { + drawMonthTitle(canvas); + drawMonthDayLabels(canvas); + drawMonthNums(canvas); + } + + private int mDayOfWeekStart = 0; + + /** + * Sets all the parameters for displaying this week. The only required + * parameter is the week number. Other parameters have a default value and + * will only update if a new value is included, except for focus month, + * which will always default to no focus month if no value is passed in. See + * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters. + * + * @param params A map of the new parameters, see + * {@link #VIEW_PARAMS_HEIGHT} + */ + public void setMonthParams(HashMap params) { + if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) { + throw new InvalidParameterException("You must specify month and year for this view"); + } + setTag(params); + // We keep the current value for any params not present + if (params.containsKey(VIEW_PARAMS_HEIGHT)) { + mRowHeight = params.get(VIEW_PARAMS_HEIGHT); + if (mRowHeight < MIN_HEIGHT) { + mRowHeight = MIN_HEIGHT; + } + } + if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) { + mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY); + } + + // Allocate space for caching the day numbers and focus values + mMonth = params.get(VIEW_PARAMS_MONTH); + mYear = params.get(VIEW_PARAMS_YEAR); + + // Figure out what day today is + //final Time today = new Time(Time.getCurrentTimezone()); + //today.setToNow(); + final PersianCalendar today = new PersianCalendar(); + mHasToday = false; + mToday = -1; + + mPersianCalendar.setPersianDate(mYear, mMonth, 1); + mDayOfWeekStart = mPersianCalendar.get(Calendar.DAY_OF_WEEK); + + if (params.containsKey(VIEW_PARAMS_WEEK_START)) { + mWeekStart = params.get(VIEW_PARAMS_WEEK_START); + } else { + mWeekStart = Calendar.SATURDAY; + } + + mNumCells = Utils.getDaysInMonth(mMonth, mYear); + for (int i = 0; i < mNumCells; i++) { + final int day = i + 1; + if (sameDay(day, today)) { + mHasToday = true; + mToday = day; + } + } + mNumRows = calculateNumRows(); + + // Invalidate cached accessibility information. + mTouchHelper.invalidateRoot(); + } + + public void setSelectedDay(int day) { + mSelectedDay = day; + } + + public void reuse() { + mNumRows = DEFAULT_NUM_ROWS; + requestLayout(); + } + + private int calculateNumRows() { + int offset = findDayOffset(); + int dividend = (offset + mNumCells) / mNumDays; + int remainder = (offset + mNumCells) % mNumDays; + return (dividend + (remainder > 0 ? 1 : 0)); + } + + private boolean sameDay(int day, PersianCalendar today) { + return mYear == today.getPersianYear() && + mMonth == today.getPersianMonth() && + day == today.getPersianDay(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows + + getMonthHeaderSize() + 5); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mWidth = w; + + // Invalidate cached accessibility information. + mTouchHelper.invalidateRoot(); + } + + public int getMonth() { + return mMonth; + } + + public int getYear() { + return mYear; + } + + /** + * A wrapper to the MonthHeaderSize to allow override it in children + */ + private int getMonthHeaderSize() { + return MONTH_HEADER_SIZE; + } + + private String getMonthAndYearString() { + mStringBuilder.setLength(0); + return LanguageUtils.getPersianNumbers( + mPersianCalendar.getPersianMonthName() + " " + mPersianCalendar.getPersianYear()); + } + + private void drawMonthTitle(Canvas canvas) { + int x = (mWidth + 2 * mEdgePadding) / 2; + int y = (getMonthHeaderSize() - MONTH_DAY_LABEL_TEXT_SIZE) / 2; + canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint); + } + + private void drawMonthDayLabels(Canvas canvas) { + int y = getMonthHeaderSize() - (MONTH_DAY_LABEL_TEXT_SIZE / 2); + int dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2); + + for (int i = 0; i < mNumDays; i++) { + int calendarDay = (i + mWeekStart) % mNumDays; + int x = (2 * i + 1) * dayWidthHalf + mEdgePadding; + mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); + String localWeekDisplayName = mDayLabelCalendar.getPersianWeekDayName(); // TODO: RTLize + String weekString = localWeekDisplayName.substring(0, 1); + canvas.drawText(weekString, x, y, mMonthDayLabelPaint); + } + } + + /** + * Draws the week and month day numbers for this week. Override this method + * if you need different placement. + * + * @param canvas The canvas to draw on + */ + private void drawMonthNums(Canvas canvas) { + int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH) + + getMonthHeaderSize(); + final float dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2.0f); + int j = findDayOffset(); + for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) { + final int x = (int) ((2 * j + 1) * dayWidthHalf + mEdgePadding); + + int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH; + + final int startX = (int) (x - dayWidthHalf); + final int stopX = (int) (x + dayWidthHalf); + final int startY = y - yRelativeToDay; + final int stopY = startY + mRowHeight; + + drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY); + + j++; + if (j == mNumDays) { + j = 0; + y += mRowHeight; + } + } + } + + /** + * This method should draw the month day. Implemented by sub-classes to allow customization. + * + * @param canvas The canvas to draw on + * @param year The year of this month day + * @param month The month of this month day + * @param day The day number of this month day + * @param x The default x position to draw the day number + * @param y The default y position to draw the day number + * @param startX The left boundary of the day number rect + * @param stopX The right boundary of the day number rect + * @param startY The top boundary of the day number rect + * @param stopY The bottom boundary of the day number rect + */ + protected abstract void drawMonthDay(Canvas canvas, int year, int month, int day, + int x, int y, int startX, int stopX, int startY, int stopY); + + private int findDayOffset() { + return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart) + - mWeekStart; + } + + + /** + * Calculates the day that the given x position is in, accounting for week + * number. Returns the day or -1 if the position wasn't in a day. + * + * @param x The x position of the touch event + * @return The day number, or -1 if the position wasn't in a day + */ + private int getDayFromLocation(float x, float y) { + final int day = getInternalDayFromLocation(x, y); + if (day < 1 || day > mNumCells) { + return -1; + } + return day; + } + + /** + * Calculates the day that the given x position is in, accounting for week + * number. + * + * @param x The x position of the touch event + * @return The day number + */ + private int getInternalDayFromLocation(float x, float y) { + int dayStart = mEdgePadding; + if (x < dayStart || x > mWidth - mEdgePadding) { + return -1; + } + // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels + int row = (int) (y - getMonthHeaderSize()) / mRowHeight; + int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mEdgePadding)); + + int day = column - findDayOffset() + 1; + day += row * mNumDays; + return day; + } + + /** + * Called when the user clicks on a day. Handles callbacks to the + * {@link OnDayClickListener} if one is set. + *

    + * If the day is out of the range set by minDate and/or maxDate, this is a no-op. + * + * @param day The day that was clicked + */ + private void onDayClick(int day) { + // If the min / max date are set, only process the click if it's a valid selection. + if (isOutOfRange(mYear, mMonth, day)) { + return; + } + + + if (mOnDayClickListener != null) { + mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day)); + } + + // This is a no-op if accessibility is turned off. + mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); + } + + /** + * @return true if the specified year/month/day are within the selectable days or the range set by minDate and maxDate. + * If one or either have not been set, they are considered as Integer.MIN_VALUE and + * Integer.MAX_VALUE. + */ + boolean isOutOfRange(int year, int month, int day) { + if (mController.getSelectableDays() != null) { + return !isSelectable(year, month, day); + } + + if (isBeforeMin(year, month, day)) { + return true; + } else if (isAfterMax(year, month, day)) { + return true; + } + + return false; + } + + private boolean isSelectable(int year, int month, int day) { + PersianCalendar[] selectableDays = mController.getSelectableDays(); + for (PersianCalendar c : selectableDays) { + if (year < c.getPersianYear()) break; + if (year > c.getPersianYear()) continue; + if (month < c.getPersianMonth()) break; + if (month > c.getPersianMonth()) continue; + if (day < c.getPersianDay()) break; + if (day > c.getPersianDay()) continue; + return true; + } + return false; + } + + private boolean isBeforeMin(int year, int month, int day) { + if (mController == null) { + return false; + } + PersianCalendar minDate = mController.getMinDate(); + if (minDate == null) { + return false; + } + + if (year < minDate.getPersianYear()) { + return true; + } else if (year > minDate.getPersianYear()) { + return false; + } + + if (month < minDate.getPersianMonth()) { + return true; + } else if (month > minDate.getPersianMonth()) { + return false; + } + + return day < minDate.getPersianDay(); + } + + private boolean isAfterMax(int year, int month, int day) { + if (mController == null) { + return false; + } + PersianCalendar maxDate = mController.getMaxDate(); + if (maxDate == null) { + return false; + } + + if (year > maxDate.getPersianYear()) { + return true; + } else if (year < maxDate.getPersianYear()) { + return false; + } + + if (month > maxDate.getPersianMonth()) { + return true; + } else if (month < maxDate.getPersianMonth()) { + return false; + } + + return day > maxDate.getPersianMonth(); + } + + /** + * @param year + * @param month + * @param day + * @return true if the given date should be highlighted + */ + boolean isHighlighted(int year, int month, int day) { + PersianCalendar[] highlightedDays = mController.getHighlightedDays(); + if (highlightedDays == null) return false; + for (PersianCalendar c : highlightedDays) { + if (year < c.getPersianYear()) break; + if (year > c.getPersianYear()) continue; + if (month < c.getPersianMonth()) break; + if (month > c.getPersianMonth()) continue; + if (day < c.getPersianDay()) break; + if (day > c.getPersianDay()) continue; + return true; + } + return false; + } + + /** + * @return The date that has accessibility focus, or {@code null} if no date + * has focus + */ + public CalendarDay getAccessibilityFocus() { + final int day = mTouchHelper.getFocusedVirtualView(); + if (day >= 0) { + return new CalendarDay(mYear, mMonth, day); + } + return null; + } + + /** + * Clears accessibility focus within the view. No-op if the view does not + * contain accessibility focus. + */ + public void clearAccessibilityFocus() { + mTouchHelper.clearFocusedVirtualView(); + } + + /** + * Attempts to restore accessibility focus to the specified date. + * + * @param day The date which should receive focus + * @return {@code false} if the date is not valid for this month view, or + * {@code true} if the date received focus + */ + public boolean restoreAccessibilityFocus(CalendarDay day) { + if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) { + return false; + } + mTouchHelper.setFocusedVirtualView(day.day); + return true; + } + + /** + * Provides a virtual view hierarchy for interfacing with an accessibility + * service. + */ + protected class MonthViewTouchHelper extends ExploreByTouchHelper { + + private Rect mTempRect = new Rect(); + private PersianCalendar mTempCalendar = new PersianCalendar(); + + public MonthViewTouchHelper(View host) { + super(host); + } + + public void setFocusedVirtualView(int virtualViewId) { + getAccessibilityNodeProvider(MonthView.this).performAction( + virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); + } + + public void clearFocusedVirtualView() { + final int focusedVirtualView = getFocusedVirtualView(); + if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) { + getAccessibilityNodeProvider(MonthView.this).performAction( + focusedVirtualView, + AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS, + null); + } + } + + @Override + protected int getVirtualViewAt(float x, float y) { + final int day = getDayFromLocation(x, y); + if (day >= 0) { + return day; + } + return ExploreByTouchHelper.INVALID_ID; + } + + @Override + protected void getVisibleVirtualViews(List virtualViewIds) { + for (int day = 1; day <= mNumCells; day++) { + virtualViewIds.add(day); + } + } + + @Override + protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { + event.setContentDescription(getItemDescription(virtualViewId)); + } + + @Override + protected void onPopulateNodeForVirtualView(int virtualViewId, + AccessibilityNodeInfoCompat node) { + getItemBounds(virtualViewId, mTempRect); + + node.setContentDescription(getItemDescription(virtualViewId)); + node.setBoundsInParent(mTempRect); + node.addAction(AccessibilityNodeInfo.ACTION_CLICK); + + if (virtualViewId == mSelectedDay) { + node.setSelected(true); + } + + } + + @Override + protected boolean onPerformActionForVirtualView(int virtualViewId, int action, + Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_CLICK: + onDayClick(virtualViewId); + return true; + } + + return false; + } + + /** + * Calculates the bounding rectangle of a given time object. + * + * @param day The day to calculate bounds for + * @param rect The rectangle in which to store the bounds + */ + void getItemBounds(int day, Rect rect) { + final int offsetX = mEdgePadding; + final int offsetY = getMonthHeaderSize(); + final int cellHeight = mRowHeight; + final int cellWidth = ((mWidth - (2 * mEdgePadding)) / mNumDays); + final int index = ((day - 1) + findDayOffset()); + final int row = (index / mNumDays); + final int column = (index % mNumDays); + final int x = (offsetX + (column * cellWidth)); + final int y = (offsetY + (row * cellHeight)); + + rect.set(x, y, (x + cellWidth), (y + cellHeight)); + } + + /** + * Generates a description for a given time object. Since this + * description will be spoken, the components are ordered by descending + * specificity as DAY MONTH YEAR. + * + * @param day The day to generate a description for + * @return A description of the time object + */ + CharSequence getItemDescription(int day) { + mTempCalendar.setPersianDate(mYear, mMonth, day); + final String date = LanguageUtils.getPersianNumbers(mTempCalendar.getPersianLongDate()); + + if (day == mSelectedDay) { + return getContext().getString(R.string.mdtp_item_is_selected, date); + } + + return date; + } + } + + /** + * Handles callbacks when the user clicks on a time object. + */ + public interface OnDayClickListener { + void onDayClick(MonthView view, CalendarDay day); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/SimpleDayPickerView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/SimpleDayPickerView.java new file mode 100644 index 0000000..0a4988e --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/SimpleDayPickerView.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.date; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * A DayPickerView customized for {@link SimpleMonthAdapter} + */ +public class SimpleDayPickerView extends DayPickerView { + + public SimpleDayPickerView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SimpleDayPickerView(Context context, DatePickerController controller) { + super(context, controller); + } + + @Override + public MonthAdapter createMonthAdapter(Context context, DatePickerController controller) { + return new SimpleMonthAdapter(context, controller); + } + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/SimpleMonthAdapter.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/SimpleMonthAdapter.java new file mode 100644 index 0000000..67f8a79 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/SimpleMonthAdapter.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.date; + +import android.content.Context; + +/** + * An adapter for a list of {@link SimpleMonthView} items. + */ +public class SimpleMonthAdapter extends MonthAdapter { + + public SimpleMonthAdapter(Context context, DatePickerController controller) { + super(context, controller); + } + + @Override + public MonthView createMonthView(Context context) { + return new SimpleMonthView(context, null, mController); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/SimpleMonthView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/SimpleMonthView.java new file mode 100644 index 0000000..3fdadd0 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/SimpleMonthView.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.date; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Typeface; +import android.util.AttributeSet; + +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; + +public class SimpleMonthView extends MonthView { + + public SimpleMonthView(Context context, AttributeSet attr, DatePickerController controller) { + super(context, attr, controller); + } + + @Override + public void drawMonthDay(Canvas canvas, int year, int month, int day, + int x, int y, int startX, int stopX, int startY, int stopY) { + if (mSelectedDay == day) { + canvas.drawCircle(x , y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE, + mSelectedCirclePaint); + } + + if(isHighlighted(year, month, day)) { + mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); + } + else { + mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)); + } + + // If we have a mindate or maxdate, gray out the day number if it's outside the range. + if (isOutOfRange(year, month, day)) { + mMonthNumPaint.setColor(mDisabledDayTextColor); + } + else if (mSelectedDay == day) { + mMonthNumPaint.setColor(mSelectedDayTextColor); + } else if (mHasToday && mToday == day) { + mMonthNumPaint.setColor(mTodayNumberColor); + } else { + mMonthNumPaint.setColor(isHighlighted(year, month, day) ? mHighlightedDayTextColor : mDayTextColor); + } + + canvas.drawText(LanguageUtils. + getPersianNumbers(String.format("%d", day)), x, y, mMonthNumPaint); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/TextViewWithCircularIndicator.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/TextViewWithCircularIndicator.java new file mode 100644 index 0000000..33696c5 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/TextViewWithCircularIndicator.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.date; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.support.annotation.NonNull; +import android.util.AttributeSet; + +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.R; + +/** + * A text view which, when pressed or activated, displays a colored circle around the text. + */ +public class TextViewWithCircularIndicator extends android.support.v7.widget.AppCompatTextView { + + private static final int SELECTED_CIRCLE_ALPHA = 255; + + private Paint mCirclePaint = new Paint(); + + private int mCircleColor; + private String mItemIsSelectedText; + + private boolean mDrawCircle; + + public TextViewWithCircularIndicator(Context context, AttributeSet attrs) { + super(context, attrs); + Resources res = context.getResources(); + mCircleColor = res.getColor(R.color.mdtp_accent_color); + int mRadius = res.getDimensionPixelOffset(R.dimen.mdtp_month_select_circle_radius); + mItemIsSelectedText = context.getResources().getString(R.string.mdtp_item_is_selected); + + init(); + } + + private void init() { + mCirclePaint.setFakeBoldText(true); + mCirclePaint.setAntiAlias(true); + mCirclePaint.setColor(mCircleColor); + mCirclePaint.setTextAlign(Align.CENTER); + mCirclePaint.setStyle(Style.FILL); + mCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA); + } + + public void drawIndicator(boolean drawCircle) { + mDrawCircle = drawCircle; + } + + @Override + public void onDraw(@NonNull Canvas canvas) { + if (mDrawCircle) { + final int width = getWidth(); + final int height = getHeight(); + int radius = Math.min(width, height) / 2; + canvas.drawCircle(width / 2, height / 2, radius, mCirclePaint); + } + setSelected(mDrawCircle); + super.onDraw(canvas); + } + + @Override + public CharSequence getContentDescription() { + String itemText = LanguageUtils.getPersianNumbers(getText().toString()); + if (mDrawCircle) { + return String.format(mItemIsSelectedText, itemText); + } else { + return itemText; + } + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/YearPickerView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/YearPickerView.java new file mode 100644 index 0000000..0a74c30 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/date/YearPickerView.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.date; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.StateListDrawable; +import android.support.annotation.NonNull; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import java.util.ArrayList; +import java.util.List; + +import mohammadaminha.com.widgets.Date_Picker.date.DatePickerDialog.OnDateChangedListener; +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.R; + +/** + * Displays a selectable list of years. + */ +public class YearPickerView extends ListView implements OnItemClickListener, OnDateChangedListener { + private static final String TAG = "YearPickerView"; + + private DatePickerController mController; + private YearAdapter mAdapter; + private int mViewSize; + private int mChildSize; + private TextViewWithCircularIndicator mSelectedView; + + /** + * @param context + */ + public YearPickerView(Context context, DatePickerController controller) { + super(context); + mController = controller; + mController.registerOnDateChangedListener(this); + ViewGroup.LayoutParams frame = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT); + setLayoutParams(frame); + Resources res = context.getResources(); + mViewSize = res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height); + mChildSize = res.getDimensionPixelOffset(R.dimen.mdtp_year_label_height); + setVerticalFadingEdgeEnabled(true); + setFadingEdgeLength(mChildSize / 3); + init(context); + setOnItemClickListener(this); + setSelector(new StateListDrawable()); + setDividerHeight(0); + onDateChanged(); + } + + private void init(Context context) { + ArrayList years = new ArrayList<>(); + for (int year = mController.getMinYear(); year <= mController.getMaxYear(); year++) { + years.add(String.format("%d", year)); + } + years = LanguageUtils.getPersianNumbers(years); + mAdapter = new YearAdapter(context, R.layout.mdtp_year_label_text_view, years); + setAdapter(mAdapter); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + mController.tryVibrate(); + TextViewWithCircularIndicator clickedView = (TextViewWithCircularIndicator) view; + if (clickedView != null) { + if (clickedView != mSelectedView) { + if (mSelectedView != null) { + mSelectedView.drawIndicator(false); + mSelectedView.requestLayout(); + } + clickedView.drawIndicator(true); + clickedView.requestLayout(); + mSelectedView = clickedView; + } + mController.onYearSelected(getYearFromTextView(clickedView)); + mAdapter.notifyDataSetChanged(); + } + } + + private static int getYearFromTextView(TextViewWithCircularIndicator view) { + return Integer.valueOf(LanguageUtils.getLatinNumbers(view.getText().toString())); + } + + private class YearAdapter extends ArrayAdapter { + + public YearAdapter(Context context, int resource, List objects) { + super(context, resource, objects); + } + + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) { + TextViewWithCircularIndicator v = (TextViewWithCircularIndicator) + super.getView(position, convertView, parent); + v.requestLayout(); + int year = getYearFromTextView(v); + boolean selected = mController.getSelectedDay().year == year; + v.drawIndicator(selected); + if (selected) { + mSelectedView = v; + } + return v; + } + } + + private void postSetSelectionCentered(final int position) { + postSetSelectionFromTop(position, mViewSize / 2 - mChildSize / 2); + } + + public void postSetSelectionFromTop(final int position, final int offset) { + post(new Runnable() { + + @Override + public void run() { + setSelectionFromTop(position, offset); + requestLayout(); + } + }); + } + + public int getFirstPositionOffset() { + final View firstChild = getChildAt(0); + if (firstChild == null) { + return 0; + } + return firstChild.getTop(); + } + + @Override + public void onDateChanged() { + mAdapter.notifyDataSetChanged(); + postSetSelectionCentered(mController.getSelectedDay().year - mController.getMinYear()); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + event.setFromIndex(0); + event.setToIndex(0); + } + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/DatePickerController.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/DatePickerController.java new file mode 100644 index 0000000..58ec6bc --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/DatePickerController.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.multidate; + +import java.util.ArrayList; + +import mohammadaminha.com.widgets.Date_Picker.utils.PersianCalendar; + +/** + * Controller class to communicate among the various components of the date picker dialog. + */ +public interface DatePickerController { + + void onYearSelected(int year); + + void onDaysOfMonthSelected(ArrayList selectedDays); + + void registerOnDateChangedListener(MultiDatePickerDialog.OnDateChangedListener listener); + + void unregisterOnDateChangedListener(MultiDatePickerDialog.OnDateChangedListener listener); + + ArrayList getSelectedDays(); + + void setSelectedDays(ArrayList selectedDays); + + boolean isThemeDark(); + + PersianCalendar[] getHighlightedDays(); + + PersianCalendar[] getSelectableDays(); + + int getFirstDayOfWeek(); + + int getMinYear(); + + int getMaxYear(); + + int getSelectedYear(); + + PersianCalendar getMinDate(); + + PersianCalendar getMaxDate(); + + void tryVibrate(); +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/DayPickerView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/DayPickerView.java new file mode 100644 index 0000000..b5133d6 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/DayPickerView.java @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.multidate; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.ListView; + +import mohammadaminha.com.widgets.Date_Picker.Utils; +import mohammadaminha.com.widgets.Date_Picker.multidate.MultiDatePickerDialog.OnDateChangedListener; +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.Date_Picker.utils.PersianCalendar; + +/** + * This displays a list of months in a calendar format with selectable days. + */ +public abstract class DayPickerView extends ListView implements OnScrollListener, + OnDateChangedListener { + + private static final String TAG = "MonthFragment"; + + // Affects when the month selection will change while scrolling up + protected static final int SCROLL_HYST_WEEKS = 2; + // How long the GoTo fling animation should last + private static final int GOTO_SCROLL_DURATION = 250; + // How long to wait after receiving an onScrollStateChanged notification + // before acting on it + private static final int SCROLL_CHANGE_DELAY = 40; + // The number of days to display in each week + public static final int DAYS_PER_WEEK = 7; + private static final int LIST_TOP_OFFSET = -1; // so that the top line will be + // under the separator + // You can override these numbers to get a different appearance + protected int mNumWeeks = 6; + protected boolean mShowWeekNumber = false; + protected int mDaysPerWeek = 7; + // + // These affect the scroll speed and feel + private float mFriction = 1.0f; + + private Context mContext; + private Handler mHandler; + + // highlighted time + private MonthAdapter.CalendarDay mSelectedDay = new MonthAdapter.CalendarDay(); + private MonthAdapter mAdapter; + + private MonthAdapter.CalendarDay mTempDay = new MonthAdapter.CalendarDay(); + + // When the week starts; numbered like Time. (e.g. SUNDAY=0). + protected int mFirstDayOfWeek; + // The last name announced by accessibility + protected CharSequence mPrevMonthName; + // which month should be displayed/highlighted [0-11] + private int mCurrentMonthDisplayed; + // used for tracking during a scroll + private long mPreviousScrollPosition; + // used for tracking what state listview is in + private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; + // used for tracking what state listview is in + private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + private DatePickerController mController; + private boolean mPerformingScroll; + + public DayPickerView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + DayPickerView(Context context, DatePickerController controller) { + super(context); + init(context); + setController(controller); + } + + private void setController(DatePickerController controller) { + mController = controller; + mController.registerOnDateChangedListener(this); + refreshAdapter(); + onDateChanged(); + } + + private void init(Context context) { + mHandler = new Handler(); + setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + setDrawSelectorOnTop(false); + + mContext = context; + setUpListView(); + } + + public void onChange() { + refreshAdapter(); + } + + /** + * Creates a new adapter if necessary and sets up its parameters. Override + * this method to provide a custom adapter. + */ + private void refreshAdapter() { + if (mAdapter == null) { + mAdapter = createMonthAdapter(getContext(), mController); +// } else { +// mAdapter.setSelectedDay(mSelectedDay); +// } + // refresh the view with the new parameters + setAdapter(mAdapter); + } + } + + protected abstract MonthAdapter createMonthAdapter(Context context, + DatePickerController controller); + + /* + * Sets all the required fields for the list view. Override this method to + * set a different list view behavior. + */ + private void setUpListView() { + // Transparent background on scroll + setCacheColorHint(0); + // No dividers + setDivider(null); + // Items are clickable + setItemsCanFocus(true); + // The thumb gets in the way, so disable it + setFastScrollEnabled(false); + setVerticalScrollBarEnabled(false); + setOnScrollListener(this); + setFadingEdgeLength(0); + // Make the scrolling behavior nicer + setFriction(ViewConfiguration.getScrollFriction() * mFriction); + } + + /** + * This moves to the specified time in the view. If the time is not already + * in range it will move the list so that the first of the month containing + * the time is at the top of the view. If the new time is already in view + * the list will not be scrolled unless forceScroll is true. This time may + * optionally be highlighted as selected as well. + * + * @param day The day to move to + * @param animate Whether to scroll to the given time or just redraw at the + * new location + * @param setSelected Whether to set the given time as selected + * @param forceScroll Whether to recenter even if the time is already + * visible + * @return Whether or not the view animated to the new location + */ + private void goTo(MonthAdapter.CalendarDay day, boolean animate, boolean setSelected, boolean forceScroll) { + + // Set the selected day + if (setSelected) { + mSelectedDay.set(day); + } + + mTempDay.set(day); + final int position = (day.year - mController.getMinYear()) + * MonthAdapter.MONTHS_IN_YEAR + day.month; + + View child; + int i = 0; + int top ; + // Find a child that's completely in the view + do { + child = getChildAt(i++); + if (child == null) { + break; + } + top = child.getTop(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "child at " + (i - 1) + " has top " + top); + } + } while (top < 0); + + // Compute the first and last position visible + int selectedPosition; + if (child != null) { + selectedPosition = getPositionForView(child); + } else { + selectedPosition = 0; + } + + /*if (setSelected) { + mAdapter.setSelectedDay(mSelectedDay); + }*/ + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "GoTo position " + position); + } + // Check if the selected day is now outside of our visible range + // and if so scroll to the month that contains it + if (position != selectedPosition || forceScroll) { + setMonthDisplayed(mTempDay); + mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; + if (animate) { + smoothScrollToPositionFromTop( + position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION); + } else { + postSetSelection(position); + } + } else if (setSelected) { + setMonthDisplayed(mSelectedDay); + } + } + + public void postSetSelection(final int position) { + clearFocus(); + post(new Runnable() { + + @Override + public void run() { + setSelection(position); + } + }); + onScrollStateChanged(this, OnScrollListener.SCROLL_STATE_IDLE); + } + + /** + * Updates the title and selected month if the view has moved to a new + * month. + */ + @Override + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + MonthView child = (MonthView) view.getChildAt(0); + if (child == null) { + return; + } + + // Figure out where we are + mPreviousScrollPosition = (long) (view.getFirstVisiblePosition() * child.getHeight() - child.getBottom()); + mPreviousScrollState = mCurrentScrollState; + } + + /** + * Sets the month displayed at the top of this view based on time. Override + * to add custom events when the title is changed. + */ + private void setMonthDisplayed(MonthAdapter.CalendarDay date) { + mCurrentMonthDisplayed = date.month; + invalidateViews(); + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + // use a post to prevent re-entering onScrollStateChanged before it + // exits + mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); + } + + private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); + + protected class ScrollStateRunnable implements Runnable { + private int mNewState; + + /** + * Sets up the runnable with a short delay in case the scroll state + * immediately changes again. + * + * @param view The list view that changed state + * @param scrollState The new state it changed to + */ + public void doScrollStateChange(AbsListView view, int scrollState) { + mHandler.removeCallbacks(this); + mNewState = scrollState; + mHandler.postDelayed(this, SCROLL_CHANGE_DELAY); + } + + @Override + public void run() { + mCurrentScrollState = mNewState; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, + "new scroll state: " + mNewState + " old state: " + mPreviousScrollState); + } + // Fix the position after a scroll or a fling ends + if (mNewState == OnScrollListener.SCROLL_STATE_IDLE + && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE + && mPreviousScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + mPreviousScrollState = mNewState; + int i = 0; + View child = getChildAt(i); + while (child != null && child.getBottom() <= 0) { + child = getChildAt(++i); + } + if (child == null) { + // The view is no longer visible, just return + return; + } + int firstPosition = getFirstVisiblePosition(); + int lastPosition = getLastVisiblePosition(); + boolean scroll = firstPosition != 0 && lastPosition != getCount() - 1; + final int top = child.getTop(); + final int bottom = child.getBottom(); + final int midpoint = getHeight() / 2; + if (scroll && top < LIST_TOP_OFFSET) { + if (bottom > midpoint) { + smoothScrollBy(top, GOTO_SCROLL_DURATION); + } else { + smoothScrollBy(bottom, GOTO_SCROLL_DURATION); + } + } + } else { + mPreviousScrollState = mNewState; + } + } + } + + /** + * Gets the position of the view that is most prominently displayed within the list view. + */ + public int getMostVisiblePosition() { + final int firstPosition = getFirstVisiblePosition(); + final int height = getHeight(); + + int maxDisplayedHeight = 0; + int mostVisibleIndex = 0; + int i = 0; + int bottom = 0; + while (bottom < height) { + View child = getChildAt(i); + if (child == null) { + break; + } + bottom = child.getBottom(); + int displayedHeight = Math.min(bottom, height) - Math.max(0, child.getTop()); + if (displayedHeight > maxDisplayedHeight) { + mostVisibleIndex = i; + maxDisplayedHeight = displayedHeight; + } + i++; + } + return firstPosition + mostVisibleIndex; + } + + @Override + public void onDateChanged() { + PersianCalendar persianCalendar = new PersianCalendar(mController.getSelectedDays() + .get(mController.getSelectedDays().size() - 1).getTimeInMillis()); + persianCalendar.setPersianDate(mController.getSelectedYear() + , persianCalendar.getPersianMonth(), persianCalendar.getPersianDay()); + goTo(new MonthAdapter.CalendarDay(persianCalendar), false, true, true); + } + + /** + * Attempts to return the date that has accessibility focus. + * + * @return The date that has accessibility focus, or {@code null} if no date + * has focus. + */ + private MonthAdapter.CalendarDay findAccessibilityFocus() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child instanceof MonthView) { + final MonthAdapter.CalendarDay focus = ((MonthView) child).getAccessibilityFocus(); + if (focus != null) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { + // Clear focus to avoid ListView bug in Jelly Bean MR1. + ((MonthView) child).clearAccessibilityFocus(); + } + return focus; + } + } + } + + return null; + } + + /** + * Attempts to restore accessibility focus to a given date. No-op if + * {@code day} is {@code null}. + * + * @param day The date that should receive accessibility focus + * @return {@code true} if focus was restored + */ + private void restoreAccessibilityFocus(MonthAdapter.CalendarDay day) { + if (day == null) { + return; + } + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child instanceof MonthView) { + if (((MonthView) child).restoreAccessibilityFocus(day)) { + return; + } + } + } + + } + + @Override + protected void layoutChildren() { + final MonthAdapter.CalendarDay focusedDay = findAccessibilityFocus(); + super.layoutChildren(); + if (mPerformingScroll) { + mPerformingScroll = false; + } else { + restoreAccessibilityFocus(focusedDay); + } + } + + @Override + public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setItemCount(-1); + } + + private static String getMonthAndYearString(MonthAdapter.CalendarDay day) { + PersianCalendar mPersianCalendar = new PersianCalendar(); + mPersianCalendar.setPersianDate(day.year, day.month, day.day); + + String sbuf = ""; + sbuf += mPersianCalendar.getPersianMonthName(); + sbuf += " "; + sbuf += mPersianCalendar.getPersianYear(); + return sbuf; + } + + /** + * Necessary for accessibility, to ensure we support "scrolling" forward and backward + * in the month list. + */ + @Override + @SuppressWarnings("deprecation") + public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + if (Build.VERSION.SDK_INT >= 21) { + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); + } else { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + + /** + * When scroll forward/backward events are received, announce the newly scrolled-to month. + */ + @SuppressLint("NewApi") + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (action != AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && + action != AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + return super.performAccessibilityAction(action, arguments); + } + + // Figure out what month is showing. + int firstVisiblePosition = getFirstVisiblePosition(); + int month = firstVisiblePosition % 12; + int year = firstVisiblePosition / 12 + mController.getMinYear(); + MonthAdapter.CalendarDay day = new MonthAdapter.CalendarDay(year, month, 1); + + // Scroll either forward or backward one month. + if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { + day.month++; + if (day.month == 12) { + day.month = 0; + day.year++; + } + } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + View firstVisibleView = getChildAt(0); + // If the view is fully visible, jump one month back. Otherwise, we'll just jump + // to the first day of first visible month. + if (firstVisibleView != null && firstVisibleView.getTop() >= -1) { + // There's an off-by-one somewhere, so the top of the first visible item will + // actually be -1 when it's at the exact top. + day.month--; + if (day.month == -1) { + day.month = 11; + day.year--; + } + } + } + + // Go to that month. + Utils.tryAccessibilityAnnounce(this, + LanguageUtils.getPersianNumbers(getMonthAndYearString(day))); + goTo(day, true, false, true); + mPerformingScroll = true; + return true; + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/MonthAdapter.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/MonthAdapter.java new file mode 100644 index 0000000..4fb7319 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/MonthAdapter.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.multidate; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView.LayoutParams; +import android.widget.BaseAdapter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; + +import mohammadaminha.com.widgets.Date_Picker.multidate.MonthView.OnDayClickListener; +import mohammadaminha.com.widgets.Date_Picker.utils.PersianCalendar; + +/** + * An adapter for a list of {@link MonthView} items. + */ +public abstract class MonthAdapter extends BaseAdapter implements OnDayClickListener { + + private static final String TAG = "SimpleMonthAdapter"; + + private Context mContext; + final DatePickerController mController; + + private ArrayList mSelectedDays; + + protected static int WEEK_7_OVERHANG_HEIGHT = 7; + static final int MONTHS_IN_YEAR = 12; + + /** + * A convenience class to represent a specific date. + */ + public static class CalendarDay { + private PersianCalendar mPersianCalendar; + int year; + int month; + int day; + + public CalendarDay() { + setTime(System.currentTimeMillis()); + } + + public CalendarDay(long timeInMillis) { + setTime(timeInMillis); + } + + public CalendarDay(PersianCalendar calendar) { + year = calendar.getPersianYear(); + month = calendar.getPersianMonth(); + day = calendar.getPersianDay(); + } + + public CalendarDay(int year, int month, int day) { + setDay(year, month, day); + } + + public void set(CalendarDay date) { + year = date.year; + month = date.month; + day = date.day; + } + + public void setDay(int year, int month, int day) { + this.year = year; + this.month = month; + this.day = day; + } + + private void setTime(long timeInMillis) { + if (mPersianCalendar == null) { + mPersianCalendar = new PersianCalendar(); + } + mPersianCalendar.setTimeInMillis(timeInMillis); + month = mPersianCalendar.getPersianMonth(); + year = mPersianCalendar.getPersianYear(); + day = mPersianCalendar.getPersianDay(); + } + + public boolean same(CalendarDay date) { + return date.day == day && date.year == year && date.month == month; + } + + public int getYear() { + return year; + } + + public int getMonth() { + return month; + } + + public int getDay() { + return day; + } + + public PersianCalendar getPersianCalendar() { + if (mPersianCalendar != null) + return mPersianCalendar; + else { + mPersianCalendar = new PersianCalendar(); + mPersianCalendar.setPersianDate(year, month, day); + return mPersianCalendar; + } + } + } + + MonthAdapter(Context context, + DatePickerController controller) { + mContext = context; + mController = controller; + mSelectedDays = mController.getSelectedDays(); + /*ArrayList persianCalendars = mController.getSelectedDays(); + for (PersianCalendar row : persianCalendars) + mSelectedDays.add(new CalendarDay(row));*/ + } + + @Override + public int getCount() { + return ((mController.getMaxYear() - mController.getMinYear()) + 1) * MONTHS_IN_YEAR; + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @SuppressLint("NewApi") + @SuppressWarnings("unchecked") + @Override + public View getView(int position, View convertView, ViewGroup parent) { + MonthView v; + HashMap drawingParams = null; + if (convertView != null) { + v = (MonthView) convertView; + // We store the drawing parameters in the view so it can be recycled + drawingParams = (HashMap) v.getTag(); + } else { + v = createMonthView(mContext); + // Set up the new view + LayoutParams params = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + v.setLayoutParams(params); + v.setClickable(true); + v.setOnDayClickListener(this); + } + if (drawingParams == null) { + drawingParams = new HashMap<>(); + } + drawingParams.clear(); + + final int month = position % MONTHS_IN_YEAR; + final int year = position / MONTHS_IN_YEAR + mController.getMinYear(); + + ArrayList days = new ArrayList<>(); + for (PersianCalendar persianCalendar : mSelectedDays) + if (isSelectedDayInMonth(new CalendarDay(persianCalendar), year, month)) + days.add(persianCalendar.getPersianDay()); + + + // Invokes requestLayout() to ensure that the recycled view is set with the appropriate + // height/number of weeks before being displayed. + v.reuse(); + + drawingParams.put(MonthView.VIEW_PARAMS_SELECTED_DAYS, days); + drawingParams.put(MonthView.VIEW_PARAMS_YEAR, year); + drawingParams.put(MonthView.VIEW_PARAMS_MONTH, month); + drawingParams.put(MonthView.VIEW_PARAMS_WEEK_START, mController.getFirstDayOfWeek()); + v.setMonthParams(drawingParams); + v.invalidate(); + return v; + } + + protected abstract MonthView createMonthView(Context context); + + private boolean isSelectedDayInMonth(CalendarDay selectedDay, int year, int month) { + return selectedDay.year == year && selectedDay.month == month; + } + + + @Override + public void onDayClick(MonthView view, CalendarDay day) { + if (day != null) { + onDayTapped(day); + } + } + + /** + * Maintains the same hour/min/sec but moves the day to the tapped day. + * + * @param day The day that was tapped + */ + private void onDayTapped(CalendarDay day) { + mController.tryVibrate(); + notifySelectedDays(day); + notifyDataSetChanged(); + mController.onDaysOfMonthSelected(mSelectedDays); + } + + private void notifySelectedDays(CalendarDay day) { + PersianCalendar toRemove = null; + for (PersianCalendar calendarDay : mSelectedDays) + if (day.same(new CalendarDay(calendarDay))) { + toRemove = calendarDay; + break; + } + + if (mSelectedDays.size() > 1 && toRemove != null) + mSelectedDays.remove(toRemove); + else { + mSelectedDays.add(day.getPersianCalendar()); + Collections.sort(mSelectedDays, new Comparator() { + @Override + public int compare(PersianCalendar o1, PersianCalendar o2) { + return o1.getTimeInMillis() > o2.getTimeInMillis() ? 1 : 0; + } + }); + } + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/MonthView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/MonthView.java new file mode 100644 index 0000000..0bbbeba --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/MonthView.java @@ -0,0 +1,836 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.multidate; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.widget.ExploreByTouchHelper; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; + +import mohammadaminha.com.widgets.Date_Picker.TypefaceHelper; +import mohammadaminha.com.widgets.Date_Picker.Utils; +import mohammadaminha.com.widgets.Date_Picker.multidate.MonthAdapter.CalendarDay; +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.Date_Picker.utils.PersianCalendar; +import mohammadaminha.com.widgets.R; +import mohammadaminha.com.widgets.Util; + +/** + * A calendar-like view displaying a specified month and the appropriate selectable day numbers + * within the specified month. + */ +public abstract class MonthView extends View { + private static final String TAG = "MonthView"; + + /** + * This sets the height of this week in pixels + */ + private static final String VIEW_PARAMS_HEIGHT = "height"; + /** + * This specifies the position (or weeks since the epoch) of this week. + */ + public static final String VIEW_PARAMS_MONTH = "month"; + /** + * This specifies the position (or weeks since the epoch) of this week. + */ + public static final String VIEW_PARAMS_YEAR = "year"; + /** + * This sets one of the days in this view as selected {@link Calendar#SUNDAY} + * through {@link Calendar#SATURDAY}. + */ + private static final String VIEW_PARAMS_SELECTED_DAY = "selected_day"; + public static final String VIEW_PARAMS_SELECTED_DAYS = "selected_days"; + /** + * Which day the week should start on. {@link Calendar#SUNDAY} through + * {@link Calendar#SATURDAY}. + */ + public static final String VIEW_PARAMS_WEEK_START = "week_start"; + /** + * How many days to display at a time. Days will be displayed starting with + * {@link #mWeekStart}. + */ + public static final String VIEW_PARAMS_NUM_DAYS = "num_days"; + /** + * Which month is currently in focus, as defined by {@link Calendar#MONTH} + * [0-11]. + */ + public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month"; + /** + * If this month should display week numbers. false if 0, true otherwise. + */ + public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num"; + + private static final int DEFAULT_HEIGHT = 32; + private static final int MIN_HEIGHT = 10; + private static final int DEFAULT_SELECTED_DAY = -1; + private static final int DEFAULT_WEEK_START = Calendar.SATURDAY; + private static final int DEFAULT_NUM_DAYS = 7; + protected static final int DEFAULT_SHOW_WK_NUM = 0; + protected static final int DEFAULT_FOCUS_MONTH = -1; + private static final int DEFAULT_NUM_ROWS = 6; + private static final int MAX_NUM_ROWS = 6; + + private static final int SELECTED_CIRCLE_ALPHA = 255; + + private static final int DAY_SEPARATOR_WIDTH = 1; + static int MINI_DAY_NUMBER_TEXT_SIZE; + private static int MONTH_LABEL_TEXT_SIZE; + private static int MONTH_DAY_LABEL_TEXT_SIZE; + private static int MONTH_HEADER_SIZE; + static int DAY_SELECTED_CIRCLE_SIZE; + + // used for scaling to the device density + protected static float mScale = 0; + + private DatePickerController mController; + + // affects the padding on the sides of this view + private int mEdgePadding = 0; + + + Paint mMonthNumPaint; + private Paint mMonthTitlePaint; + Paint mSelectedCirclePaint; + private Paint mMonthDayLabelPaint; + + private StringBuilder mStringBuilder; + + // The Julian day of the first day displayed by this item + protected int mFirstJulianDay = -1; + // The month of the first day in this week + protected int mFirstMonth = -1; + // The month of the last day in this week + protected int mLastMonth = -1; + + private int mMonth; + + private int mYear; + // Quick reference to the width of this view, matches parent + private int mWidth; + // The height this view should draw at in pixels, set by height param + private int mRowHeight = DEFAULT_HEIGHT; + // If this view contains the today + boolean mHasToday = false; + // Which day is selected [0-6] or -1 if no day is selected + private int mSelectedDay = -1; + ArrayList mSelectedDays = new ArrayList<>(); + // Which day is today [0-6] or -1 if no day is today + int mToday = DEFAULT_SELECTED_DAY; + // Which day of the week to start on [0-6] + private int mWeekStart = DEFAULT_WEEK_START; + // How many days to display + private int mNumDays = DEFAULT_NUM_DAYS; + // The number of days + a spot for week number if it is displayed + private int mNumCells = mNumDays; + // The left edge of the selected day + protected int mSelectedLeft = -1; + // The right edge of the selected day + protected int mSelectedRight = -1; + + private PersianCalendar mPersianCalendar; + private PersianCalendar mDayLabelCalendar; + private MonthViewTouchHelper mTouchHelper; + + private int mNumRows = DEFAULT_NUM_ROWS; + + // Optional listener for handling day click actions + private OnDayClickListener mOnDayClickListener; + + // Whether to prevent setting the accessibility delegate + private boolean mLockAccessibilityDelegate; + + final int mDayTextColor; + final int mSelectedDayTextColor; + private int mMonthDayTextColor; + final int mTodayNumberColor; + final int mHighlightedDayTextColor; + final int mDisabledDayTextColor; + private int mMonthTitleColor; + + public MonthView(Context context) { + this(context, null, null); + } + + MonthView(Context context, AttributeSet attr, DatePickerController controller) { + super(context, attr); + mController = controller; + Resources res = context.getResources(); + + mDayLabelCalendar = new PersianCalendar(); + mPersianCalendar = new PersianCalendar(); + + + boolean darkTheme = mController != null && mController.isThemeDark(); + if(darkTheme) { + mDayTextColor = res.getColor(R.color.mdtp_date_picker_text_normal_dark_theme); + mMonthDayTextColor = res.getColor(R.color.mdtp_date_picker_month_day_dark_theme); + mDisabledDayTextColor = res.getColor(R.color.mdtp_date_picker_text_disabled_dark_theme); + mHighlightedDayTextColor = res.getColor(R.color.mdtp_date_picker_text_highlighted_dark_theme); + } + else { + mDayTextColor = res.getColor(R.color.mdtp_date_picker_text_normal); + mMonthDayTextColor = res.getColor(R.color.mdtp_date_picker_month_day); + mDisabledDayTextColor = res.getColor(R.color.mdtp_date_picker_text_disabled); + mHighlightedDayTextColor = res.getColor(R.color.mdtp_date_picker_text_highlighted); + } + mSelectedDayTextColor = res.getColor(R.color.mdtp_white); + mTodayNumberColor = res.getColor(R.color.mdtp_accent_color); + mMonthTitleColor = res.getColor(R.color.mdtp_white); + + mStringBuilder = new StringBuilder(50); + + MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_day_number_size); + MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_month_label_size); + MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_month_day_label_text_size); + MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.mdtp_month_list_item_header_height); + DAY_SELECTED_CIRCLE_SIZE = res + .getDimensionPixelSize(R.dimen.mdtp_day_number_select_circle_radius); + + mRowHeight = (res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height) + - getMonthHeaderSize()) / MAX_NUM_ROWS; + + // Set up accessibility components. + mTouchHelper = getMonthViewTouchHelper(); + ViewCompat.setAccessibilityDelegate(this, mTouchHelper); + ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + mLockAccessibilityDelegate = true; + + // Sets up any standard paints that will be used + initView(); + } + + public void setDatePickerController(DatePickerController controller) { + mController = controller; + } + + private MonthViewTouchHelper getMonthViewTouchHelper() { + return new MonthViewTouchHelper(this); + } + + @Override + public void setAccessibilityDelegate(AccessibilityDelegate delegate) { + // Workaround for a JB MR1 issue where accessibility delegates on + // top-level ListView items are overwritten. + if (!mLockAccessibilityDelegate) { + super.setAccessibilityDelegate(delegate); + } + } + + public void setOnDayClickListener(OnDayClickListener listener) { + mOnDayClickListener = listener; + } + + @Override + public boolean dispatchHoverEvent(@NonNull MotionEvent event) { + // First right-of-refusal goes the touch exploration helper. + return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event); + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + final int day = getDayFromLocation(event.getX(), event.getY()); + if (day >= 0) { + onDayClick(day); + } + break; + } + return true; + } + + /** + * Sets up the text and style properties for painting. Override this if you + * want to use a different paint. + */ + private void initView() { + mMonthTitlePaint = new Paint(); + mMonthTitlePaint.setFakeBoldText(true); + mMonthTitlePaint.setAntiAlias(true); + mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE); + + mMonthTitlePaint.setTypeface(Util.getTypeFace()); + mMonthTitlePaint.setColor(mDayTextColor); + mMonthTitlePaint.setTextAlign(Align.CENTER); + mMonthTitlePaint.setStyle(Style.FILL); + + mSelectedCirclePaint = new Paint(); + mSelectedCirclePaint.setFakeBoldText(true); + mSelectedCirclePaint.setAntiAlias(true); + mSelectedCirclePaint.setColor(mTodayNumberColor); + mSelectedCirclePaint.setTextAlign(Align.CENTER); + mSelectedCirclePaint.setStyle(Style.FILL); + mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA); + + mMonthDayLabelPaint = new Paint(); + mMonthDayLabelPaint.setAntiAlias(true); + mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE); + mMonthDayLabelPaint.setColor(mMonthDayTextColor); + mMonthDayLabelPaint.setTypeface(TypefaceHelper.get(getContext(),"Roboto-Medium")); + mMonthDayLabelPaint.setStyle(Style.FILL); + mMonthDayLabelPaint.setTextAlign(Align.CENTER); + mMonthDayLabelPaint.setFakeBoldText(true); + + mMonthNumPaint = new Paint(); + mMonthNumPaint.setAntiAlias(true); + mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE); + mMonthNumPaint.setStyle(Style.FILL); + mMonthNumPaint.setTextAlign(Align.CENTER); + mMonthNumPaint.setFakeBoldText(false); + } + + @Override + protected void onDraw(Canvas canvas) { + drawMonthTitle(canvas); + drawMonthDayLabels(canvas); + drawMonthNums(canvas); + } + + private int mDayOfWeekStart = 0; + + /** + * Sets all the parameters for displaying this week. The only required + * parameter is the week number. Other parameters have a default value and + * will only update if a new value is included, except for focus month, + * which will always default to no focus month if no value is passed in. See + * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters. + * + * @param params A map of the new parameters, see + * {@link #VIEW_PARAMS_HEIGHT} + */ + public void setMonthParams(HashMap params) { + if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) { + throw new InvalidParameterException("You must specify month and year for this view"); + } + setTag(params); + // We keep the current value for any params not present + if (params.containsKey(VIEW_PARAMS_HEIGHT)) { + mRowHeight = (int) params.get(VIEW_PARAMS_HEIGHT); + if (mRowHeight < MIN_HEIGHT) { + mRowHeight = MIN_HEIGHT; + } + } + if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) { + mSelectedDay = (int) params.get(VIEW_PARAMS_SELECTED_DAY); + } + if (params.containsKey(VIEW_PARAMS_SELECTED_DAYS)) { + mSelectedDays = (ArrayList) params.get(VIEW_PARAMS_SELECTED_DAYS); + } + // Allocate space for caching the day numbers and focus values + mMonth = (int) params.get(VIEW_PARAMS_MONTH); + mYear = (int) params.get(VIEW_PARAMS_YEAR); + + // Figure out what day today is + //final Time today = new Time(Time.getCurrentTimezone()); + //today.setToNow(); + final PersianCalendar today = new PersianCalendar(); + mHasToday = false; + mToday = -1; + + mPersianCalendar.setPersianDate(mYear, mMonth, 1); + mDayOfWeekStart = mPersianCalendar.get(Calendar.DAY_OF_WEEK); + + if (params.containsKey(VIEW_PARAMS_WEEK_START)) { + mWeekStart = (int) params.get(VIEW_PARAMS_WEEK_START); + } else { + mWeekStart = Calendar.SATURDAY; + } + + mNumCells = Utils.getDaysInMonth(mMonth, mYear); + for (int i = 0; i < mNumCells; i++) { + final int day = i + 1; + if (sameDay(day, today)) { + mHasToday = true; + mToday = day; + } + } + mNumRows = calculateNumRows(); + + // Invalidate cached accessibility information. + mTouchHelper.invalidateRoot(); + } + + public void setSelectedDay(int day) { + mSelectedDay = day; + } + + public void reuse() { + mNumRows = DEFAULT_NUM_ROWS; + requestLayout(); + } + + private int calculateNumRows() { + int offset = findDayOffset(); + int dividend = (offset + mNumCells) / mNumDays; + int remainder = (offset + mNumCells) % mNumDays; + return (dividend + (remainder > 0 ? 1 : 0)); + } + + private boolean sameDay(int day, PersianCalendar today) { + return mYear == today.getPersianYear() && + mMonth == today.getPersianMonth() && + day == today.getPersianDay(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows + + getMonthHeaderSize() + 5); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mWidth = w; + + // Invalidate cached accessibility information. + mTouchHelper.invalidateRoot(); + } + + public int getMonth() { + return mMonth; + } + + public int getYear() { + return mYear; + } + + /** + * A wrapper to the MonthHeaderSize to allow override it in children + */ + private int getMonthHeaderSize() { + return MONTH_HEADER_SIZE; + } + + private String getMonthAndYearString() { + mStringBuilder.setLength(0); + return LanguageUtils.getPersianNumbers( + mPersianCalendar.getPersianMonthName() + " " + mPersianCalendar.getPersianYear()); + } + + private void drawMonthTitle(Canvas canvas) { + int x = (mWidth + 2 * mEdgePadding) / 2; + int y = (getMonthHeaderSize() - MONTH_DAY_LABEL_TEXT_SIZE) / 2; + canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint); + } + + private void drawMonthDayLabels(Canvas canvas) { + int y = getMonthHeaderSize() - (MONTH_DAY_LABEL_TEXT_SIZE / 2); + int dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2); + + for (int i = 0; i < mNumDays; i++) { + int calendarDay = (i + mWeekStart) % mNumDays; + int x = (2 * i + 1) * dayWidthHalf + mEdgePadding; + mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); + String localWeekDisplayName = mDayLabelCalendar.getPersianWeekDayName(); // TODO: RTLize + String weekString = localWeekDisplayName.substring(0, 1); + canvas.drawText(weekString, x, y, mMonthDayLabelPaint); + } + } + + /** + * Draws the week and month day numbers for this week. Override this method + * if you need different placement. + * + * @param canvas The canvas to draw on + */ + private void drawMonthNums(Canvas canvas) { + int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH) + + getMonthHeaderSize(); + final float dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2.0f); + int j = findDayOffset(); + for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) { + final int x = (int)((2 * j + 1) * dayWidthHalf + mEdgePadding); + + int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH; + + final int startX = (int)(x - dayWidthHalf); + final int stopX = (int)(x + dayWidthHalf); + final int startY = y - yRelativeToDay; + final int stopY = startY + mRowHeight; + + drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY); + + j++; + if (j == mNumDays) { + j = 0; + y += mRowHeight; + } + } + } + + /** + * This method should draw the month day. Implemented by sub-classes to allow customization. + * + * @param canvas The canvas to draw on + * @param year The year of this month day + * @param month The month of this month day + * @param day The day number of this month day + * @param x The default x position to draw the day number + * @param y The default y position to draw the day number + * @param startX The left boundary of the day number rect + * @param stopX The right boundary of the day number rect + * @param startY The top boundary of the day number rect + * @param stopY The bottom boundary of the day number rect + */ + protected abstract void drawMonthDay(Canvas canvas, int year, int month, int day, + int x, int y, int startX, int stopX, int startY, int stopY); + + private int findDayOffset() { + return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart) + - mWeekStart; + } + + + /** + * Calculates the day that the given x position is in, accounting for week + * number. Returns the day or -1 if the position wasn't in a day. + * + * @param x The x position of the touch event + * @return The day number, or -1 if the position wasn't in a day + */ + private int getDayFromLocation(float x, float y) { + final int day = getInternalDayFromLocation(x, y); + if (day < 1 || day > mNumCells) { + return -1; + } + return day; + } + + /** + * Calculates the day that the given x position is in, accounting for week + * number. + * + * @param x The x position of the touch event + * @return The day number + */ + private int getInternalDayFromLocation(float x, float y) { + int dayStart = mEdgePadding; + if (x < dayStart || x > mWidth - mEdgePadding) { + return -1; + } + // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels + int row = (int) (y - getMonthHeaderSize()) / mRowHeight; + int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mEdgePadding)); + + int day = column - findDayOffset() + 1; + day += row * mNumDays; + return day; + } + + /** + * Called when the user clicks on a day. Handles callbacks to the + * {@link OnDayClickListener} if one is set. + *

    + * If the day is out of the range set by minDate and/or maxDate, this is a no-op. + * + * @param day The day that was clicked + */ + private void onDayClick(int day) { + // If the min / max date are set, only process the click if it's a valid selection. + if (isOutOfRange(mYear, mMonth, day)) { + return; + } + + + if (mOnDayClickListener != null) { + mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day)); + } + + // This is a no-op if accessibility is turned off. + mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); + } + + /** + * @return true if the specified year/month/day are within the selectable days or the range set by minDate and maxDate. + * If one or either have not been set, they are considered as Integer.MIN_VALUE and + * Integer.MAX_VALUE. + */ + boolean isOutOfRange(int year, int month, int day) { + if (mController.getSelectableDays() != null) { + return !isSelectable(year, month, day); + } + + if (isBeforeMin(year, month, day)) { + return true; + } + else if (isAfterMax(year, month, day)) { + return true; + } + + return false; + } + + private boolean isSelectable(int year, int month, int day) { + PersianCalendar[] selectableDays = mController.getSelectableDays(); + for (PersianCalendar c : selectableDays) { + if(year < c.getPersianYear()) break; + if(year > c.getPersianYear()) continue; + if(month < c.getPersianMonth()) break; + if(month > c.getPersianMonth()) continue; + if(day < c.getPersianDay()) break; + if(day > c.getPersianDay()) continue; + return true; + } + return false; + } + + private boolean isBeforeMin(int year, int month, int day) { + if (mController == null) { + return false; + } + PersianCalendar minDate = mController.getMinDate(); + if (minDate == null) { + return false; + } + + if (year < minDate.getPersianYear()) { + return true; + } else if (year > minDate.getPersianYear()) { + return false; + } + + if (month < minDate.getPersianMonth()) { + return true; + } else if (month > minDate.getPersianMonth()) { + return false; + } + + return day < minDate.getPersianDay(); + } + + private boolean isAfterMax(int year, int month, int day) { + if (mController == null) { + return false; + } + PersianCalendar maxDate = mController.getMaxDate(); + if (maxDate == null) { + return false; + } + + if (year > maxDate.getPersianYear()) { + return true; + } else if (year < maxDate.getPersianYear()) { + return false; + } + + if (month > maxDate.getPersianMonth()) { + return true; + } else if (month < maxDate.getPersianMonth()) { + return false; + } + + return day > maxDate.getPersianMonth(); + } + + /** + * @param year + * @param month + * @param day + * @return true if the given date should be highlighted + */ + boolean isHighlighted(int year, int month, int day) { + PersianCalendar[] highlightedDays = mController.getHighlightedDays(); + if(highlightedDays == null) return false; + for (PersianCalendar c : highlightedDays) { + if(year < c.getPersianYear()) break; + if(year > c.getPersianYear()) continue; + if(month < c.getPersianMonth()) break; + if(month > c.getPersianMonth()) continue; + if(day < c.getPersianDay()) break; + if(day > c.getPersianDay()) continue; + return true; + } + return false; + } + + /** + * @return The date that has accessibility focus, or {@code null} if no date + * has focus + */ + public CalendarDay getAccessibilityFocus() { + final int day = mTouchHelper.getFocusedVirtualView(); + if (day >= 0) { + return new CalendarDay(mYear, mMonth, day); + } + return null; + } + + /** + * Clears accessibility focus within the view. No-op if the view does not + * contain accessibility focus. + */ + public void clearAccessibilityFocus() { + mTouchHelper.clearFocusedVirtualView(); + } + + /** + * Attempts to restore accessibility focus to the specified date. + * + * @param day The date which should receive focus + * @return {@code false} if the date is not valid for this month view, or + * {@code true} if the date received focus + */ + public boolean restoreAccessibilityFocus(CalendarDay day) { + if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) { + return false; + } + mTouchHelper.setFocusedVirtualView(day.day); + return true; + } + + /** + * Provides a virtual view hierarchy for interfacing with an accessibility + * service. + */ + protected class MonthViewTouchHelper extends ExploreByTouchHelper { + + private Rect mTempRect = new Rect(); + private PersianCalendar mTempCalendar = new PersianCalendar(); + + public MonthViewTouchHelper(View host) { + super(host); + } + + public void setFocusedVirtualView(int virtualViewId) { + getAccessibilityNodeProvider(MonthView.this).performAction( + virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); + } + + public void clearFocusedVirtualView() { + final int focusedVirtualView = getFocusedVirtualView(); + if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) { + getAccessibilityNodeProvider(MonthView.this).performAction( + focusedVirtualView, + AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS, + null); + } + } + + @Override + protected int getVirtualViewAt(float x, float y) { + final int day = getDayFromLocation(x, y); + if (day >= 0) { + return day; + } + return ExploreByTouchHelper.INVALID_ID; + } + + @Override + protected void getVisibleVirtualViews(List virtualViewIds) { + for (int day = 1; day <= mNumCells; day++) { + virtualViewIds.add(day); + } + } + + @Override + protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { + event.setContentDescription(getItemDescription(virtualViewId)); + } + + @Override + protected void onPopulateNodeForVirtualView(int virtualViewId, + AccessibilityNodeInfoCompat node) { + getItemBounds(virtualViewId, mTempRect); + + node.setContentDescription(getItemDescription(virtualViewId)); + node.setBoundsInParent(mTempRect); + node.addAction(AccessibilityNodeInfo.ACTION_CLICK); + + if (virtualViewId == mSelectedDay) { + node.setSelected(true); + } + + } + + @Override + protected boolean onPerformActionForVirtualView(int virtualViewId, int action, + Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_CLICK: + onDayClick(virtualViewId); + return true; + } + + return false; + } + + /** + * Calculates the bounding rectangle of a given time object. + * + * @param day The day to calculate bounds for + * @param rect The rectangle in which to store the bounds + */ + void getItemBounds(int day, Rect rect) { + final int offsetX = mEdgePadding; + final int offsetY = getMonthHeaderSize(); + final int cellHeight = mRowHeight; + final int cellWidth = ((mWidth - (2 * mEdgePadding)) / mNumDays); + final int index = ((day - 1) + findDayOffset()); + final int row = (index / mNumDays); + final int column = (index % mNumDays); + final int x = (offsetX + (column * cellWidth)); + final int y = (offsetY + (row * cellHeight)); + + rect.set(x, y, (x + cellWidth), (y + cellHeight)); + } + + /** + * Generates a description for a given time object. Since this + * description will be spoken, the components are ordered by descending + * specificity as DAY MONTH YEAR. + * + * @param day The day to generate a description for + * @return A description of the time object + */ + CharSequence getItemDescription(int day) { + mTempCalendar.setPersianDate(mYear, mMonth, day); + final String date = LanguageUtils.getPersianNumbers(mTempCalendar.getPersianLongDate()); + + if (day == mSelectedDay) { + return getContext().getString(R.string.mdtp_item_is_selected, date); + } + + return date; + } + } + + /** + * Handles callbacks when the user clicks on a time object. + */ + interface OnDayClickListener { + void onDayClick(MonthView view, CalendarDay day); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/MultiDatePickerDialog.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/MultiDatePickerDialog.java new file mode 100644 index 0000000..3c051cf --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/MultiDatePickerDialog.java @@ -0,0 +1,650 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.multidate; + +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.widget.LinearLayout; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashSet; + +import mohammadaminha.com.widgets.Button; +import mohammadaminha.com.widgets.Date_Picker.HapticFeedbackController; +import mohammadaminha.com.widgets.Date_Picker.TypefaceHelper; +import mohammadaminha.com.widgets.Date_Picker.Utils; +import mohammadaminha.com.widgets.Date_Picker.date.AccessibleDateAnimator; +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.Date_Picker.utils.PersianCalendar; +import mohammadaminha.com.widgets.R; +import mohammadaminha.com.widgets.TextView; + +/** + * Dialog allowing users to select a date. + */ +public class MultiDatePickerDialog extends DialogFragment implements + OnClickListener, DatePickerController { + + private static final String TAG = "MultiDatePickerDialog"; + + private static final int UNINITIALIZED = -1; + private static final int MONTH_AND_DAY_VIEW = 0; + private static final int YEAR_VIEW = 1; + + private static final String KEY_SELECTED_DAYS = "selectedDays"; + private static final String KEY_LIST_POSITION = "list_position"; + private static final String KEY_WEEK_START = "week_start"; + private static final String KEY_YEAR_START = "year_start"; + private static final String KEY_YEAR_END = "year_end"; + private static final String KEY_CURRENT_VIEW = "current_view"; + private static final String KEY_LIST_POSITION_OFFSET = "list_position_offset"; + private static final String KEY_MIN_DATE = "min_date"; + private static final String KEY_MAX_DATE = "max_date"; + private static final String KEY_SELECTED_YEAR = "selected_year"; + private static final String KEY_HIGHLIGHTED_DAYS = "highlighted_days"; + private static final String KEY_SELECTABLE_DAYS = "selectable_days"; + private static final String KEY_THEME_DARK = "theme_dark"; + + private static final int DEFAULT_START_YEAR = 1350; + private static final int DEFAULT_END_YEAR = 1450; + + private static final int ANIMATION_DURATION = 300; + private static final int ANIMATION_DELAY = 500; + + private ArrayList mSelectedDaysCalendars = new ArrayList<>(); + private OnDateSetListener mCallBack; + private HashSet mListeners = new HashSet<>(); + private DialogInterface.OnCancelListener mOnCancelListener; + private DialogInterface.OnDismissListener mOnDismissListener; + + private AccessibleDateAnimator mAnimator; + + private TextView mDayOfWeekView; + private LinearLayout mMonthAndDayView; + private TextView mSelectedMonthTextView; + private TextView mSelectedDayTextView; + private TextView mYearView; + private DayPickerView mDayPickerView; + private YearPickerView mYearPickerView; + + private int mCurrentView = UNINITIALIZED; + + private int mWeekStart = PersianCalendar.SATURDAY; + private int mMinYear = DEFAULT_START_YEAR; + private int mMaxYear = DEFAULT_END_YEAR; + private int mSelectedYear; + private PersianCalendar mMinDate; + private PersianCalendar mMaxDate; + private PersianCalendar[] highlightedDays; + private PersianCalendar[] selectableDays; + private boolean mThemeDark; + + private HapticFeedbackController mHapticFeedbackController; + + private boolean mDelayAnimation = true; + + // Accessibility strings. + private String mDayPickerDescription; + private String mSelectDay; + private String mYearPickerDescription; + private String mSelectYear; + + /** + * The callback used to indicate the user is done filling in the date. + */ + public interface OnDateSetListener { + + /** + * @param view The view associated with this listener. + * @param selectedDays List of days that have been selected. + */ + void onDateSet(MultiDatePickerDialog view, ArrayList selectedDays); + } + + /** + * The callback used to notify other date picker components of a change in selected date. + */ + public interface OnDateChangedListener { + + void onDateChanged(); + } + + + public MultiDatePickerDialog() { + // Empty constructor required for dialog fragment. + } + + /** + * @param callBack How the parent is notified that the date is set. + * @param selectedDays Selected days shown on date picker. + */ + public static MultiDatePickerDialog newInstance(OnDateSetListener callBack, @Nullable ArrayList selectedDays) { + MultiDatePickerDialog ret = new MultiDatePickerDialog(); + ret.initialize(callBack, selectedDays); + return ret; + } + + private void initialize(OnDateSetListener callBack, @Nullable ArrayList selectedDays) { + mCallBack = callBack; + if (selectedDays != null) { + setSelectedDays(selectedDays); + } else { + mSelectedDaysCalendars.add(new PersianCalendar(System.currentTimeMillis())); + } + mSelectedYear = mSelectedDaysCalendars.get(mSelectedDaysCalendars.size() - 1).getPersianYear(); + mThemeDark = false; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Activity activity = getActivity(); + activity.getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + if (savedInstanceState != null) { + mSelectedDaysCalendars.clear(); + mSelectedDaysCalendars.addAll((ArrayList) savedInstanceState.getSerializable(KEY_SELECTED_DAYS)); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(KEY_SELECTED_DAYS, mSelectedDaysCalendars); + outState.putInt(KEY_WEEK_START, mWeekStart); + outState.putInt(KEY_YEAR_START, mMinYear); + outState.putInt(KEY_YEAR_END, mMaxYear); + outState.putInt(KEY_CURRENT_VIEW, mCurrentView); + int listPosition = -1; + if (mCurrentView == MONTH_AND_DAY_VIEW) { + listPosition = mDayPickerView.getMostVisiblePosition(); + } else if (mCurrentView == YEAR_VIEW) { + listPosition = mYearPickerView.getFirstVisiblePosition(); + outState.putInt(KEY_LIST_POSITION_OFFSET, mYearPickerView.getFirstPositionOffset()); + } + outState.putInt(KEY_LIST_POSITION, listPosition); + outState.putSerializable(KEY_MIN_DATE, mMinDate); + outState.putSerializable(KEY_MAX_DATE, mMaxDate); + outState.putSerializable(KEY_SELECTED_YEAR, mSelectedYear); + outState.putSerializable(KEY_HIGHLIGHTED_DAYS, highlightedDays); + outState.putSerializable(KEY_SELECTABLE_DAYS, selectableDays); + outState.putBoolean(KEY_THEME_DARK, mThemeDark); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Log.d(TAG, "onCreateView: "); + getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE); + + View view = inflater.inflate(R.layout.mdtp_date_picker_dialog, null); + + mDayOfWeekView = view.findViewById(R.id.date_picker_header); + mMonthAndDayView = view.findViewById(R.id.date_picker_month_and_day); + mMonthAndDayView.setOnClickListener(this); + mSelectedMonthTextView = view.findViewById(R.id.date_picker_month); + mSelectedDayTextView = view.findViewById(R.id.date_picker_day); + mYearView = view.findViewById(R.id.date_picker_year); + mYearView.setOnClickListener(this); + + int listPosition = -1; + int listPositionOffset = 0; + int currentView = MONTH_AND_DAY_VIEW; + if (savedInstanceState != null) { + mWeekStart = savedInstanceState.getInt(KEY_WEEK_START); + mMinYear = savedInstanceState.getInt(KEY_YEAR_START); + mMaxYear = savedInstanceState.getInt(KEY_YEAR_END); + mSelectedYear = savedInstanceState.getInt(KEY_SELECTED_YEAR); + currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW); + listPosition = savedInstanceState.getInt(KEY_LIST_POSITION); + listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET); + mMinDate = (PersianCalendar) savedInstanceState.getSerializable(KEY_MIN_DATE); + mMaxDate = (PersianCalendar) savedInstanceState.getSerializable(KEY_MAX_DATE); + highlightedDays = (PersianCalendar[]) savedInstanceState.getSerializable(KEY_HIGHLIGHTED_DAYS); + selectableDays = (PersianCalendar[]) savedInstanceState.getSerializable(KEY_SELECTABLE_DAYS); + mThemeDark = savedInstanceState.getBoolean(KEY_THEME_DARK); + } + + final Activity activity = getActivity(); + mDayPickerView = new SimpleDayPickerView(activity, this); + mYearPickerView = new YearPickerView(activity, this); + + Resources res = getResources(); + mDayPickerDescription = res.getString(R.string.mdtp_day_picker_description); + mSelectDay = res.getString(R.string.mdtp_select_day); + mYearPickerDescription = res.getString(R.string.mdtp_year_picker_description); + mSelectYear = res.getString(R.string.mdtp_select_year); + + int bgColorResource = mThemeDark ? R.color.mdtp_date_picker_view_animator_dark_theme : R.color.mdtp_date_picker_view_animator; + view.setBackgroundColor(activity.getResources().getColor(bgColorResource)); + + mAnimator = view.findViewById(R.id.animator); + mAnimator.addView(mDayPickerView); + mAnimator.addView(mYearPickerView); + mAnimator.setDateMillis(mSelectedDaysCalendars.get(mSelectedDaysCalendars.size() - 1).getTimeInMillis()); + // TODO: Replace with animation decided upon by the design team. + Animation animation = new AlphaAnimation(0.0f, 1.0f); + animation.setDuration(ANIMATION_DURATION); + mAnimator.setInAnimation(animation); + // TODO: Replace with animation decided upon by the design team. + Animation animation2 = new AlphaAnimation(1.0f, 0.0f); + animation2.setDuration(ANIMATION_DURATION); + mAnimator.setOutAnimation(animation2); + + Button okButton = view.findViewById(R.id.ok); + okButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + tryVibrate(); + if (mCallBack != null) { + mCallBack.onDateSet(MultiDatePickerDialog.this, mSelectedDaysCalendars); + } + dismiss(); + } + }); + okButton.setTypeface(TypefaceHelper.get(activity, "Roboto-Medium")); + + Button cancelButton = view.findViewById(R.id.cancel); + cancelButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + tryVibrate(); + getDialog().cancel(); + } + }); + cancelButton.setTypeface(TypefaceHelper.get(activity, "Roboto-Medium")); + cancelButton.setVisibility(isCancelable() ? View.VISIBLE : View.GONE); + + updateDisplay(false); + setCurrentView(currentView); + + if (listPosition != -1) { + if (currentView == MONTH_AND_DAY_VIEW) { + mDayPickerView.postSetSelection(listPosition); + } else if (currentView == YEAR_VIEW) { + mYearPickerView.postSetSelectionFromTop(listPosition, listPositionOffset); + } + } + + mHapticFeedbackController = new HapticFeedbackController(activity); + return view; + } + + @Override + public void onResume() { + super.onResume(); + mHapticFeedbackController.start(); + } + + @Override + public void onPause() { + super.onPause(); + mHapticFeedbackController.stop(); + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + if (mOnCancelListener != null) mOnCancelListener.onCancel(dialog); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (mOnDismissListener != null) mOnDismissListener.onDismiss(dialog); + } + + private void setCurrentView(final int viewIndex) { + + switch (viewIndex) { + case MONTH_AND_DAY_VIEW: + ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mMonthAndDayView, 0.9f, + 1.05f); + if (mDelayAnimation) { + pulseAnimator.setStartDelay(ANIMATION_DELAY); + mDelayAnimation = false; + } + mDayPickerView.onDateChanged(); + if (mCurrentView != viewIndex) { + mMonthAndDayView.setSelected(true); + mYearView.setSelected(false); + mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW); + mCurrentView = viewIndex; + } + pulseAnimator.start(); + + String dayString = LanguageUtils.getPersianNumbers(mSelectedDaysCalendars.get(mSelectedDaysCalendars.size() - 1).getPersianLongDate()); + mAnimator.setContentDescription(mDayPickerDescription + ": " + dayString); + Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay); + break; + case YEAR_VIEW: + pulseAnimator = Utils.getPulseAnimator(mYearView, 0.85f, 1.1f); + if (mDelayAnimation) { + pulseAnimator.setStartDelay(ANIMATION_DELAY); + mDelayAnimation = false; + } + mYearPickerView.onDateChanged(); + if (mCurrentView != viewIndex) { + mMonthAndDayView.setSelected(false); + mYearView.setSelected(true); + mAnimator.setDisplayedChild(YEAR_VIEW); + mCurrentView = viewIndex; + } + pulseAnimator.start(); + + String yearString = LanguageUtils. + getPersianNumbers(String.valueOf(mSelectedDaysCalendars.get(mSelectedDaysCalendars.size() - 1).getPersianYear())); + mAnimator.setContentDescription(mYearPickerDescription + ": " + yearString); + Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear); + break; + } + } + + private void updateDisplay(boolean announce) { + if (mSelectedDaysCalendars.size() == 0) + return; + PersianCalendar target = mSelectedDaysCalendars.get(mSelectedDaysCalendars.size() - 1); + if (mDayOfWeekView != null) { + mDayOfWeekView.setText(target.getPersianWeekDayName()); + } + + mSelectedMonthTextView.setText(LanguageUtils. + getPersianNumbers(target.getPersianMonthName())); + mSelectedDayTextView.setText(LanguageUtils. + getPersianNumbers(String.valueOf(target.getPersianDay()))); + mYearView.setText(LanguageUtils. + getPersianNumbers(String.valueOf(mSelectedYear))); + + // Accessibility. + long millis = target.getTimeInMillis(); + mAnimator.setDateMillis(millis); + String monthAndDayText = LanguageUtils.getPersianNumbers( + target.getPersianMonthName() + " " + + target.getPersianDay() + ); + mMonthAndDayView.setContentDescription(monthAndDayText); + + if (announce) { + String fullDateText = LanguageUtils. + getPersianNumbers(target.getPersianLongDate()); + Utils.tryAccessibilityAnnounce(mAnimator, fullDateText); + } + } + + /** + * Set whether the dark theme should be used + * + * @param themeDark true if the dark theme should be used, false if the default theme should be used + */ + public void setThemeDark(boolean themeDark) { + mThemeDark = themeDark; + } + + /** + * Returns true when the dark theme should be used + * + * @return true if the dark theme should be used, false if the default theme should be used + */ + @Override + public boolean isThemeDark() { + return mThemeDark; + } + + @SuppressWarnings("unused") + public void setFirstDayOfWeek(int startOfWeek) { + if (startOfWeek < Calendar.SUNDAY || startOfWeek > Calendar.SATURDAY) { + throw new IllegalArgumentException("Value must be between Calendar.SUNDAY and " + + "Calendar.SATURDAY"); + } + mWeekStart = startOfWeek; + if (mDayPickerView != null) { + mDayPickerView.onChange(); + } + } + + @SuppressWarnings("unused") + public void setYearRange(int startYear, int endYear) { + if (endYear < startYear) { + throw new IllegalArgumentException("Year end must be larger than or equal to year start"); + } + + mMinYear = startYear; + mMaxYear = endYear; + if (mDayPickerView != null) { + mDayPickerView.onChange(); + } + } + + /** + * Sets the minimal date supported by this DatePicker. Dates before (but not including) the + * specified date will be disallowed from being selected. + * + * @param calendar a Calendar object set to the year, month, day desired as the mindate. + */ + @SuppressWarnings("unused") + public void setMinDate(PersianCalendar calendar) { + mMinDate = calendar; + + if (mDayPickerView != null) { + mDayPickerView.onChange(); + } + } + + /** + * @return The minimal date supported by this DatePicker. Null if it has not been set. + */ + @Override + public PersianCalendar getMinDate() { + return mMinDate; + } + + /** + * Sets the minimal date supported by this DatePicker. Dates after (but not including) the + * specified date will be disallowed from being selected. + * + * @param calendar a Calendar object set to the year, month, day desired as the maxdate. + */ + @SuppressWarnings("unused") + public void setMaxDate(PersianCalendar calendar) { + mMaxDate = calendar; + + if (mDayPickerView != null) { + mDayPickerView.onChange(); + } + } + + /** + * @return The maximal date supported by this DatePicker. Null if it has not been set. + */ + @Override + public PersianCalendar getMaxDate() { + return mMaxDate; + } + + /** + * Sets an array of dates which should be highlighted when the picker is drawn + * + * @param highlightedDays an Array of Calendar objects containing the dates to be highlighted + */ + @SuppressWarnings("unused") + public void setHighlightedDays(PersianCalendar[] highlightedDays) { + // Sort the array to optimize searching over it later on + Arrays.sort(highlightedDays); + this.highlightedDays = highlightedDays; + } + + /** + * @return The list of dates, as Calendar Objects, which should be highlighted. null is no dates should be highlighted + */ + @Override + public PersianCalendar[] getHighlightedDays() { + return highlightedDays; + } + + /** + * Set's a list of days which are the only valid selections. + * Setting this value will take precedence over using setMinDate() and setMaxDate() + * + * @param selectableDays an Array of Calendar Objects containing the selectable dates + */ + @SuppressWarnings("unused") + public void setSelectableDays(PersianCalendar[] selectableDays) { + // Sort the array to optimize searching over it later on + Arrays.sort(selectableDays); + this.selectableDays = selectableDays; + } + + /** + * @return an Array of Calendar objects containing the list with selectable items. null if no restriction is set + */ + @Override + public PersianCalendar[] getSelectableDays() { + return selectableDays; + } + + @SuppressWarnings("unused") + public void setOnDateSetListener(OnDateSetListener listener) { + mCallBack = listener; + } + + @SuppressWarnings("unused") + public void setOnCancelListener(DialogInterface.OnCancelListener onCancelListener) { + mOnCancelListener = onCancelListener; + } + + @SuppressWarnings("unused") + public void setOnDismissListener(DialogInterface.OnDismissListener onDismissListener) { + mOnDismissListener = onDismissListener; + } + + // If the newly selected month / year does not contain the currently selected day number, + // change the selected day number to the last day of the selected month or year. + // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30 + // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013 + private void adjustDayInMonthIfNeeded(int month, int year) { +// int day = mPersianCalendar.getPersianDay(); +// int daysInMonth = Utils.getDaysInMonth(month, year); +// if (day > daysInMonth) { +// mPersianCalendar.setPersianDate(Persian); +// } TODO + } + + @Override + public void onClick(View v) { + tryVibrate(); + if (v.getId() == R.id.date_picker_year) { + setCurrentView(YEAR_VIEW); + } else if (v.getId() == R.id.date_picker_month_and_day) { + setCurrentView(MONTH_AND_DAY_VIEW); + } + } + + @Override + public void onYearSelected(int year) { + mSelectedYear = year; + adjustDayInMonthIfNeeded(mSelectedDaysCalendars.get(mSelectedDaysCalendars.size() - 1).getPersianMonth(), year); + if (mSelectedDaysCalendars.size() == 1) + mSelectedDaysCalendars.get(mSelectedDaysCalendars.size() - 1).setPersianDate(year + , mSelectedDaysCalendars.get(mSelectedDaysCalendars.size() - 1).getPersianMonth(), + mSelectedDaysCalendars.get(mSelectedDaysCalendars.size() - 1).getPersianDay()); + updatePickers(); + setCurrentView(MONTH_AND_DAY_VIEW); + updateDisplay(true); + } + + @Override + public void onDaysOfMonthSelected(ArrayList selectedDays) { + //setSelectedDays(selectedDays); + mSelectedYear = selectedDays.get(selectedDays.size() - 1).getPersianYear(); + updatePickers(); + updateDisplay(true); + } + + private void updatePickers() { + for (OnDateChangedListener listener : mListeners) listener.onDateChanged(); + } + + + @Override + public ArrayList getSelectedDays() { + return mSelectedDaysCalendars; + } + + @Override + public void setSelectedDays(ArrayList selectedDays) { + mSelectedDaysCalendars.clear(); + mSelectedDaysCalendars.addAll(selectedDays); + } + + @Override + public int getMinYear() { + if (selectableDays != null) return selectableDays[0].getPersianYear(); + // Ensure no years can be selected outside of the given minimum date + return mMinDate != null && mMinDate.getPersianYear() > mMinYear ? mMinDate.getPersianYear() : mMinYear; + } + + @Override + public int getMaxYear() { + if (selectableDays != null) + return selectableDays[selectableDays.length - 1].getPersianYear(); + // Ensure no years can be selected outside of the given maximum date + return mMaxDate != null && mMaxDate.getPersianYear() < mMaxYear ? mMaxDate.getPersianYear() : mMaxYear; + } + + @Override + public int getSelectedYear() { + return mSelectedYear; + } + + @Override + public int getFirstDayOfWeek() { + return mWeekStart; + } + + @Override + public void registerOnDateChangedListener(OnDateChangedListener listener) { + mListeners.add(listener); + } + + @Override + public void unregisterOnDateChangedListener(OnDateChangedListener listener) { + mListeners.remove(listener); + } + + @Override + public void tryVibrate() { + mHapticFeedbackController.tryVibrate(); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/SimpleDayPickerView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/SimpleDayPickerView.java new file mode 100644 index 0000000..1bb3d50 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/SimpleDayPickerView.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.multidate; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * A DayPickerView customized for {@link SimpleMonthAdapter} + */ +public class SimpleDayPickerView extends DayPickerView { + + public SimpleDayPickerView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SimpleDayPickerView(Context context, DatePickerController controller) { + super(context, controller); + } + + @Override + public MonthAdapter createMonthAdapter(Context context, DatePickerController controller) { + return new SimpleMonthAdapter(context, controller); + } + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/SimpleMonthAdapter.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/SimpleMonthAdapter.java new file mode 100644 index 0000000..ded4e4d --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/SimpleMonthAdapter.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.multidate; + +import android.content.Context; + +/** + * An adapter for a list of {@link SimpleMonthView} items. + */ +public class SimpleMonthAdapter extends MonthAdapter { + + public SimpleMonthAdapter(Context context, DatePickerController controller) { + super(context, controller); + } + + @Override + public MonthView createMonthView(Context context) { + return new SimpleMonthView(context, null, mController); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/SimpleMonthView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/SimpleMonthView.java new file mode 100644 index 0000000..fd3bed7 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/SimpleMonthView.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.multidate; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Typeface; +import android.util.AttributeSet; + +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; + +public class SimpleMonthView extends MonthView { + + public SimpleMonthView(Context context, AttributeSet attr, DatePickerController controller) { + super(context, attr, controller); + } + + @Override + public void drawMonthDay(Canvas canvas, int year, int month, int day, + int x, int y, int startX, int stopX, int startY, int stopY) { + boolean flag = false; + for (int selectedDays : mSelectedDays) { + if (day == selectedDays) { + canvas.drawCircle(x, y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE, + mSelectedCirclePaint); + flag = true; + break; + } + } + + if (isHighlighted(year, month, day)) { + mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); + } else { + mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)); + } + + // If we have a mindate or maxdate, gray out the day number if it's outside the range. + if (isOutOfRange(year, month, day)) { + mMonthNumPaint.setColor(mDisabledDayTextColor); + } + else if (flag) { + mMonthNumPaint.setColor(mSelectedDayTextColor); + } + else if (mHasToday && mToday == day) { + mMonthNumPaint.setColor(mTodayNumberColor); + } else { + mMonthNumPaint.setColor(isHighlighted(year, month, day) ? mHighlightedDayTextColor : mDayTextColor); + } + + canvas.drawText(LanguageUtils. + getPersianNumbers(String.format("%d", day)), x, y, mMonthNumPaint); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/YearPickerView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/YearPickerView.java new file mode 100644 index 0000000..e8b9ade --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/multidate/YearPickerView.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.multidate; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.StateListDrawable; +import android.support.annotation.NonNull; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import java.util.ArrayList; +import java.util.List; + +import mohammadaminha.com.widgets.Date_Picker.date.TextViewWithCircularIndicator; +import mohammadaminha.com.widgets.Date_Picker.multidate.MultiDatePickerDialog.OnDateChangedListener; +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.R; + +/** + * Displays a selectable list of years. + */ +public class YearPickerView extends ListView implements OnItemClickListener, OnDateChangedListener { + private static final String TAG = "YearPickerView"; + + private DatePickerController mController; + private YearAdapter mAdapter; + private int mViewSize; + private int mChildSize; + private TextViewWithCircularIndicator mSelectedView; + + /** + * @param context + */ + public YearPickerView(Context context, DatePickerController controller) { + super(context); + mController = controller; + mController.registerOnDateChangedListener(this); + ViewGroup.LayoutParams frame = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT); + setLayoutParams(frame); + Resources res = context.getResources(); + mViewSize = res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height); + mChildSize = res.getDimensionPixelOffset(R.dimen.mdtp_year_label_height); + setVerticalFadingEdgeEnabled(true); + setFadingEdgeLength(mChildSize / 3); + init(context); + setOnItemClickListener(this); + setSelector(new StateListDrawable()); + setDividerHeight(0); + onDateChanged(); + } + + private void init(Context context) { + ArrayList years = new ArrayList<>(); + for (int year = mController.getMinYear(); year <= mController.getMaxYear(); year++) { + years.add(String.format("%d", year)); + } + years = LanguageUtils.getPersianNumbers(years); + mAdapter = new YearAdapter(context, R.layout.mdtp_year_label_text_view, years); + setAdapter(mAdapter); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + mController.tryVibrate(); + TextViewWithCircularIndicator clickedView = (TextViewWithCircularIndicator) view; + if (clickedView != null) { + if (clickedView != mSelectedView) { + if (mSelectedView != null) { + mSelectedView.drawIndicator(false); + mSelectedView.requestLayout(); + } + clickedView.drawIndicator(true); + clickedView.requestLayout(); + mSelectedView = clickedView; + } + mController.onYearSelected(getYearFromTextView(clickedView)); + mAdapter.notifyDataSetChanged(); + } + } + + private static int getYearFromTextView(TextViewWithCircularIndicator view) { + return Integer.valueOf(LanguageUtils.getLatinNumbers(view.getText().toString())); + } + + private class YearAdapter extends ArrayAdapter { + + public YearAdapter(Context context, int resource, List objects) { + super(context, resource, objects); + } + + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) { + TextViewWithCircularIndicator v = (TextViewWithCircularIndicator) + super.getView(position, convertView, parent); + v.requestLayout(); + int year = getYearFromTextView(v); + boolean selected = mController.getSelectedYear() == year; + v.drawIndicator(selected); + if (selected) { + mSelectedView = v; + } + return v; + } + } + + private void postSetSelectionCentered(final int position) { + postSetSelectionFromTop(position, mViewSize / 2 - mChildSize / 2); + } + + public void postSetSelectionFromTop(final int position, final int offset) { + post(new Runnable() { + + @Override + public void run() { + setSelectionFromTop(position, offset); + requestLayout(); + } + }); + } + + public int getFirstPositionOffset() { + final View firstChild = getChildAt(0); + if (firstChild == null) { + return 0; + } + return firstChild.getTop(); + } + + @Override + public void onDateChanged() { + mAdapter.notifyDataSetChanged(); + postSetSelectionCentered(mController.getSelectedYear() - mController.getMinYear()); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + event.setFromIndex(0); + event.setToIndex(0); + } + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/AmPmCirclesView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/AmPmCirclesView.java new file mode 100644 index 0000000..7597f23 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/AmPmCirclesView.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.time; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.util.Log; +import android.view.View; + +import mohammadaminha.com.widgets.Date_Picker.Utils; +import mohammadaminha.com.widgets.R; +import mohammadaminha.com.widgets.Util; + +/** + * Draw the two smaller AM and PM circles next to where the larger circle will be. + */ +public class AmPmCirclesView extends View { + private static final String TAG = "AmPmCirclesView"; + + // Alpha level for selected circle. + private static final int SELECTED_ALPHA = Utils.SELECTED_ALPHA; + private static final int SELECTED_ALPHA_THEME_DARK = Utils.SELECTED_ALPHA_THEME_DARK; + + private Paint mPaint = new Paint(); + private int mSelectedAlpha; + private int mTouchedColor; + private int mUnselectedColor; + private int mAmPmTextColor; + private int mAmPmSelectedTextColor; + private int mSelectedColor; + private float mCircleRadiusMultiplier; + private float mAmPmCircleRadiusMultiplier; + private String mAmText; + private String mPmText; + private boolean mIsInitialized; + + private static final int AM = TimePickerDialog.AM; + private static final int PM = TimePickerDialog.PM; + + private boolean mDrawValuesReady; + private int mAmPmCircleRadius; + private int mAmXCenter; + private int mPmXCenter; + private int mAmPmYCenter; + private int mAmOrPm; + private int mAmOrPmPressed; + + public AmPmCirclesView(Context context) { + super(context); + mIsInitialized = false; + } + + public void initialize(Context context, int amOrPm) { + if (mIsInitialized) { + Log.e(TAG, "AmPmCirclesView may only be initialized once."); + return; + } + + Resources res = context.getResources(); + mUnselectedColor = res.getColor(R.color.mdtp_white); + mSelectedColor = res.getColor(R.color.mdtp_accent_color); + mTouchedColor = res.getColor(R.color.mdtp_accent_color_dark); + mAmPmTextColor = res.getColor(R.color.mdtp_ampm_text_color); + mAmPmSelectedTextColor = res.getColor(R.color.mdtp_white); + mSelectedAlpha = SELECTED_ALPHA; + + mPaint.setTypeface(Util.getTypeFace()); + mPaint.setAntiAlias(true); + mPaint.setTextAlign(Align.CENTER); + + mCircleRadiusMultiplier = + Float.parseFloat(res.getString(R.string.mdtp_circle_radius_multiplier)); + mAmPmCircleRadiusMultiplier = + Float.parseFloat(res.getString(R.string.mdtp_ampm_circle_radius_multiplier)); + mAmText = "قبل‌ازظهر"; + mPmText = "بعدازظهر"; + + setAmOrPm(amOrPm); + mAmOrPmPressed = -1; + + mIsInitialized = true; + } + + /* package */ void setTheme(Context context, boolean themeDark) { + Resources res = context.getResources(); + if (themeDark) { + mUnselectedColor = res.getColor(R.color.mdtp_circle_background_dark_theme); + mSelectedColor = res.getColor(R.color.mdtp_red); + mAmPmTextColor = res.getColor(R.color.mdtp_white); + mSelectedAlpha = SELECTED_ALPHA_THEME_DARK; + } else { + mUnselectedColor = res.getColor(R.color.mdtp_white); + mSelectedColor = res.getColor(R.color.mdtp_accent_color); + mAmPmTextColor = res.getColor(R.color.mdtp_ampm_text_color); + mSelectedAlpha = SELECTED_ALPHA; + } + } + + public void setAmOrPm(int amOrPm) { + mAmOrPm = amOrPm; + } + + public void setAmOrPmPressed(int amOrPmPressed) { + mAmOrPmPressed = amOrPmPressed; + } + + /** + * Calculate whether the coordinates are touching the AM or PM circle. + */ + public int getIsTouchingAmOrPm(float xCoord, float yCoord) { + if (!mDrawValuesReady) { + return -1; + } + + int squaredYDistance = (int) ((yCoord - mAmPmYCenter)*(yCoord - mAmPmYCenter)); + + int distanceToAmCenter = + (int) Math.sqrt((xCoord - mAmXCenter)*(xCoord - mAmXCenter) + squaredYDistance); + if (distanceToAmCenter <= mAmPmCircleRadius) { + return AM; + } + + int distanceToPmCenter = + (int) Math.sqrt((xCoord - mPmXCenter)*(xCoord - mPmXCenter) + squaredYDistance); + if (distanceToPmCenter <= mAmPmCircleRadius) { + return PM; + } + + // Neither was close enough. + return -1; + } + + @Override + public void onDraw(Canvas canvas) { + int viewWidth = getWidth(); + if (viewWidth == 0 || !mIsInitialized) { + return; + } + + if (!mDrawValuesReady) { + int layoutXCenter = getWidth() / 2; + int layoutYCenter = getHeight() / 2; + int circleRadius = + (int) (Math.min(layoutXCenter, layoutYCenter) * mCircleRadiusMultiplier); + mAmPmCircleRadius = (int) (circleRadius * mAmPmCircleRadiusMultiplier); + layoutYCenter += mAmPmCircleRadius*0.75; + int textSize = mAmPmCircleRadius * 3 / 4; + mPaint.setTextSize(textSize); + + // Line up the vertical center of the AM/PM circles with the bottom of the main circle. + mAmPmYCenter = layoutYCenter - mAmPmCircleRadius / 2 + circleRadius; + // Line up the horizontal edges of the AM/PM circles with the horizontal edges + // of the main circle. + mAmXCenter = layoutXCenter - circleRadius + mAmPmCircleRadius; + mPmXCenter = layoutXCenter + circleRadius - mAmPmCircleRadius; + + mDrawValuesReady = true; + } + + // We'll need to draw either a lighter blue (for selection), a darker blue (for touching) + // or white (for not selected). + int amColor = mUnselectedColor; + int amAlpha = 255; + int amTextColor = mAmPmTextColor; + int pmColor = mUnselectedColor; + int pmAlpha = 255; + int pmTextColor = mAmPmTextColor; + + if (mAmOrPm == AM) { + amColor = mSelectedColor; + amAlpha = mSelectedAlpha; + amTextColor = mAmPmSelectedTextColor; + } else if (mAmOrPm == PM) { + pmColor = mSelectedColor; + pmAlpha = mSelectedAlpha; + pmTextColor = mAmPmSelectedTextColor; + } + if (mAmOrPmPressed == AM) { + amColor = mTouchedColor; + amAlpha = mSelectedAlpha; + } else if (mAmOrPmPressed == PM) { + pmColor = mTouchedColor; + pmAlpha = mSelectedAlpha; + } + + // Draw the two circles. + mPaint.setColor(amColor); + mPaint.setAlpha(amAlpha); + canvas.drawCircle(mAmXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaint); + mPaint.setColor(pmColor); + mPaint.setAlpha(pmAlpha); + canvas.drawCircle(mPmXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaint); + + // Draw the AM/PM texts on top. + mPaint.setColor(amTextColor); + int textYCenter = mAmPmYCenter - (int) (mPaint.descent() + mPaint.ascent()) / 2; + canvas.drawText(mAmText, mAmXCenter, textYCenter, mPaint); + mPaint.setColor(pmTextColor); + canvas.drawText(mPmText, mPmXCenter, textYCenter, mPaint); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/CircleView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/CircleView.java new file mode 100644 index 0000000..b9c1571 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/CircleView.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.time; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.Log; +import android.view.View; + +import mohammadaminha.com.widgets.R; + +/** + * Draws a simple white circle on which the numbers will be drawn. + */ +public class CircleView extends View { + private static final String TAG = "CircleView"; + + private Paint mPaint = new Paint(); + private boolean mIs24HourMode; + private int mCircleColor; + private int mDotColor; + private float mCircleRadiusMultiplier; + private float mAmPmCircleRadiusMultiplier; + private boolean mIsInitialized; + + private boolean mDrawValuesReady; + private int mXCenter; + private int mYCenter; + private int mCircleRadius; + + public CircleView(Context context) { + super(context); + + Resources res = context.getResources(); + mCircleColor = res.getColor(R.color.mdtp_circle_color); + mDotColor = res.getColor(R.color.mdtp_numbers_text_color); + mPaint.setAntiAlias(true); + + mIsInitialized = false; + } + + public void initialize(Context context, boolean is24HourMode) { + if (mIsInitialized) { + Log.e(TAG, "CircleView may only be initialized once."); + return; + } + + Resources res = context.getResources(); + mIs24HourMode = is24HourMode; + if (is24HourMode) { + mCircleRadiusMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_circle_radius_multiplier_24HourMode)); + } else { + mCircleRadiusMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_circle_radius_multiplier)); + mAmPmCircleRadiusMultiplier = + Float.parseFloat(res.getString(R.string.mdtp_ampm_circle_radius_multiplier)); + } + + mIsInitialized = true; + } + + /* package */ void setTheme(Context context, boolean dark) { + Resources res = context.getResources(); + if (dark) { + mCircleColor = res.getColor(R.color.mdtp_circle_background_dark_theme); + mDotColor = res.getColor(R.color.mdtp_white); + } else { + mCircleColor = res.getColor(R.color.mdtp_circle_color); + mDotColor = res.getColor(R.color.mdtp_numbers_text_color); + } + } + + + @Override + public void onDraw(Canvas canvas) { + int viewWidth = getWidth(); + if (viewWidth == 0 || !mIsInitialized) { + return; + } + + if (!mDrawValuesReady) { + mXCenter = getWidth() / 2; + mYCenter = getHeight() / 2; + mCircleRadius = (int) (Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier); + + if (!mIs24HourMode) { + // We'll need to draw the AM/PM circles, so the main circle will need to have + // a slightly higher center. To keep the entire view centered vertically, we'll + // have to push it up by half the radius of the AM/PM circles. + int amPmCircleRadius = (int) (mCircleRadius * mAmPmCircleRadiusMultiplier); + mYCenter -= amPmCircleRadius*0.75; + } + + mDrawValuesReady = true; + } + + // Draw the white circle. + mPaint.setColor(mCircleColor); + canvas.drawCircle(mXCenter, mYCenter, mCircleRadius, mPaint); + + // Draw a small black circle in the center. + mPaint.setColor(mDotColor); + canvas.drawCircle(mXCenter, mYCenter, 4, mPaint); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/RadialPickerLayout.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/RadialPickerLayout.java new file mode 100644 index 0000000..306a6b6 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/RadialPickerLayout.java @@ -0,0 +1,861 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.time; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.text.format.DateUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.FrameLayout; + +import java.util.Calendar; + +import mohammadaminha.com.widgets.Date_Picker.HapticFeedbackController; +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.R; + +/** + * The primary layout to hold the circular picker, and the am/pm buttons. This view will measure + * itself to end up as a square. It also handles touches to be passed in to views that need to know + * when they'd been touched. + */ +public class RadialPickerLayout extends FrameLayout implements OnTouchListener { + private static final String TAG = "RadialPickerLayout"; + + private int TOUCH_SLOP; + private int TAP_TIMEOUT; + + private static final int VISIBLE_DEGREES_STEP_SIZE = 30; + private static final int HOUR_VALUE_TO_DEGREES_STEP_SIZE = VISIBLE_DEGREES_STEP_SIZE; + private static final int MINUTE_VALUE_TO_DEGREES_STEP_SIZE = 6; + private static final int HOUR_INDEX = TimePickerDialog.HOUR_INDEX; + private static final int MINUTE_INDEX = TimePickerDialog.MINUTE_INDEX; + private static final int AMPM_INDEX = TimePickerDialog.AMPM_INDEX; + private static final int ENABLE_PICKER_INDEX = TimePickerDialog.ENABLE_PICKER_INDEX; + private static final int AM = TimePickerDialog.AM; + private static final int PM = TimePickerDialog.PM; + + private int mLastValueSelected; + + private HapticFeedbackController mHapticFeedbackController; + private OnValueSelectedListener mListener; + private boolean mTimeInitialized; + private int mCurrentHoursOfDay; + private int mCurrentMinutes; + private boolean mIs24HourMode; + private boolean mHideAmPm; + private int mCurrentItemShowing; + + private CircleView mCircleView; + private AmPmCirclesView mAmPmCirclesView; + private RadialTextsView mHourRadialTextsView; + private RadialTextsView mMinuteRadialTextsView; + private RadialSelectorView mHourRadialSelectorView; + private RadialSelectorView mMinuteRadialSelectorView; + private View mGrayBox; + + private int[] mSnapPrefer30sMap; + private boolean mInputEnabled; + private int mIsTouchingAmOrPm = -1; + private boolean mDoingMove; + private boolean mDoingTouch; + private int mDownDegrees; + private float mDownX; + private float mDownY; + private AccessibilityManager mAccessibilityManager; + + private AnimatorSet mTransition; + private Handler mHandler = new Handler(); + + public interface OnValueSelectedListener { + void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance); + } + + public RadialPickerLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + setOnTouchListener(this); + ViewConfiguration vc = ViewConfiguration.get(context); + TOUCH_SLOP = vc.getScaledTouchSlop(); + TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); + mDoingMove = false; + + mCircleView = new CircleView(context); + addView(mCircleView); + + mAmPmCirclesView = new AmPmCirclesView(context); + addView(mAmPmCirclesView); + + mHourRadialSelectorView = new RadialSelectorView(context); + addView(mHourRadialSelectorView); + mMinuteRadialSelectorView = new RadialSelectorView(context); + addView(mMinuteRadialSelectorView); + + mHourRadialTextsView = new RadialTextsView(context); + addView(mHourRadialTextsView); + mMinuteRadialTextsView = new RadialTextsView(context); + addView(mMinuteRadialTextsView); + + // Prepare mapping to snap touchable degrees to selectable degrees. + preparePrefer30sMap(); + + mLastValueSelected = -1; + + mInputEnabled = true; + + mGrayBox = new View(context); + mGrayBox.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + mGrayBox.setBackgroundColor(getResources().getColor(R.color.mdtp_transparent_black)); + mGrayBox.setVisibility(View.INVISIBLE); + addView(mGrayBox); + + mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); + + mTimeInitialized = false; + } + + /** + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int measuredHeight = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int minDimension = Math.min(measuredWidth, measuredHeight); + + super.onMeasure(MeasureSpec.makeMeasureSpec(minDimension, widthMode), + MeasureSpec.makeMeasureSpec(minDimension, heightMode)); + } + **/ + + public void setOnValueSelectedListener(OnValueSelectedListener listener) { + mListener = listener; + } + + /** + * Initialize the Layout with starting values. + * @param context + * @param initialHoursOfDay + * @param initialMinutes + * @param is24HourMode + */ + public void initialize(Context context, HapticFeedbackController hapticFeedbackController, + int initialHoursOfDay, int initialMinutes, boolean is24HourMode) { + if (mTimeInitialized) { + Log.e(TAG, "Time has already been initialized."); + return; + } + + mHapticFeedbackController = hapticFeedbackController; + mIs24HourMode = is24HourMode; + mHideAmPm = mAccessibilityManager.isTouchExplorationEnabled() || mIs24HourMode; + + // Initialize the circle and AM/PM circles if applicable. + mCircleView.initialize(context, mHideAmPm); + mCircleView.invalidate(); + if (!mHideAmPm) { + mAmPmCirclesView.initialize(context, initialHoursOfDay < 12? AM : PM); + mAmPmCirclesView.invalidate(); + } + + // Initialize the hours and minutes numbers. + Resources res = context.getResources(); + int[] hours = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + int[] hours_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}; + int[] minutes = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}; + String[] hoursTexts = new String[12]; + String[] innerHoursTexts = new String[12]; + String[] minutesTexts = new String[12]; + for (int i = 0; i < 12; i++) { + hoursTexts[i] = LanguageUtils.getPersianNumbers( + is24HourMode? String.format("%02d", hours_24[i]) : String.format("%d", hours[i]) + ); + innerHoursTexts[i] = LanguageUtils.getPersianNumbers(String.format("%d", hours[i])); + minutesTexts[i] = LanguageUtils.getPersianNumbers(String.format("%02d", minutes[i])); + } + mHourRadialTextsView.initialize(res, + hoursTexts, (is24HourMode? innerHoursTexts : null), mHideAmPm, true); + mHourRadialTextsView.setSelection(is24HourMode ? initialHoursOfDay : initialHoursOfDay % 12); + mHourRadialTextsView.invalidate(); + mMinuteRadialTextsView.initialize(res, minutesTexts, null, mHideAmPm, false); + mMinuteRadialTextsView.setSelection(initialMinutes); + mMinuteRadialTextsView.invalidate(); + + // Initialize the currently-selected hour and minute. + setValueForItem(HOUR_INDEX, initialHoursOfDay); + setValueForItem(MINUTE_INDEX, initialMinutes); + int hourDegrees = (initialHoursOfDay % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE; + mHourRadialSelectorView.initialize(context, mHideAmPm, is24HourMode, true, + hourDegrees, isHourInnerCircle(initialHoursOfDay)); + int minuteDegrees = initialMinutes * MINUTE_VALUE_TO_DEGREES_STEP_SIZE; + mMinuteRadialSelectorView.initialize(context, mHideAmPm, false, false, + minuteDegrees, false); + + mTimeInitialized = true; + } + + /* package */ void setTheme(Context context, boolean themeDark) { + mCircleView.setTheme(context, themeDark); + mAmPmCirclesView.setTheme(context, themeDark); + mHourRadialTextsView.setTheme(context, themeDark); + mMinuteRadialTextsView.setTheme(context, themeDark); + mHourRadialSelectorView.setTheme(context, themeDark); + mMinuteRadialSelectorView.setTheme(context, themeDark); + } + + public void setTime(int hours, int minutes) { + setItem(HOUR_INDEX, hours); + setItem(MINUTE_INDEX, minutes); + } + + /** + * Set either the hour or the minute. Will set the internal value, and set the selection. + */ + private void setItem(int index, int value) { + if (index == HOUR_INDEX) { + setValueForItem(HOUR_INDEX, value); + int hourDegrees = (value % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE; + mHourRadialSelectorView.setSelection(hourDegrees, isHourInnerCircle(value), false); + mHourRadialSelectorView.invalidate(); + mHourRadialTextsView.setSelection(value); + mHourRadialTextsView.invalidate(); + } else if (index == MINUTE_INDEX) { + setValueForItem(MINUTE_INDEX, value); + int minuteDegrees = value * MINUTE_VALUE_TO_DEGREES_STEP_SIZE; + mMinuteRadialSelectorView.setSelection(minuteDegrees, false, false); + mMinuteRadialSelectorView.invalidate(); + mMinuteRadialTextsView.setSelection(value); + mHourRadialTextsView.invalidate(); + } + } + + /** + * Check if a given hour appears in the outer circle or the inner circle + * @return true if the hour is in the inner circle, false if it's in the outer circle. + */ + private boolean isHourInnerCircle(int hourOfDay) { + // We'll have the 00 hours on the outside circle. + return mIs24HourMode && (hourOfDay <= 12 && hourOfDay != 0); + } + + public int getHours() { + return mCurrentHoursOfDay; + } + + public int getMinutes() { + return mCurrentMinutes; + } + + /** + * If the hours are showing, return the current hour. If the minutes are showing, return the + * current minute. + */ + private int getCurrentlyShowingValue() { + int currentIndex = getCurrentItemShowing(); + if (currentIndex == HOUR_INDEX) { + return mCurrentHoursOfDay; + } else if (currentIndex == MINUTE_INDEX) { + return mCurrentMinutes; + } else { + return -1; + } + } + + public int getIsCurrentlyAmOrPm() { + if (mCurrentHoursOfDay < 12) { + return AM; + } else if (mCurrentHoursOfDay < 24) { + return PM; + } + return -1; + } + + /** + * Set the internal value for the hour, minute, or AM/PM. + */ + private void setValueForItem(int index, int value) { + if (index == HOUR_INDEX) { + mCurrentHoursOfDay = value; + } else if (index == MINUTE_INDEX){ + mCurrentMinutes = value; + } else if (index == AMPM_INDEX) { + if (value == AM) { + mCurrentHoursOfDay = mCurrentHoursOfDay % 12; + } else if (value == PM) { + mCurrentHoursOfDay = (mCurrentHoursOfDay % 12) + 12; + } + } + } + + /** + * Set the internal value as either AM or PM, and update the AM/PM circle displays. + * @param amOrPm + */ + public void setAmOrPm(int amOrPm) { + mAmPmCirclesView.setAmOrPm(amOrPm); + mAmPmCirclesView.invalidate(); + setValueForItem(AMPM_INDEX, amOrPm); + } + + /** + * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger + * selectable area to each of the 12 visible values, such that the ratio of space apportioned + * to a visible value : space apportioned to a non-visible value will be 14 : 4. + * E.g. the output of 30 degrees should have a higher range of input associated with it than + * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock + * circle (5 on the minutes, 1 or 13 on the hours). + */ + private void preparePrefer30sMap() { + // We'll split up the visible output and the non-visible output such that each visible + // output will correspond to a range of 14 associated input degrees, and each non-visible + // output will correspond to a range of 4 associate input degrees, so visible numbers + // are more than 3 times easier to get than non-visible numbers: + // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc. + // + // If an output of 30 degrees should correspond to a range of 14 associated degrees, then + // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should + // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you + // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this + // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the + // ability to aggressively prefer the visible values by a factor of more than 3:1, which + // greatly contributes to the selectability of these values. + + // Our input will be 0 through 360. + mSnapPrefer30sMap = new int[361]; + + // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}. + int snappedOutputDegrees = 0; + // Count of how many inputs we've designated to the specified output. + int count = 1; + // How many input we expect for a specified output. This will be 14 for output divisible + // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so + // the caller can decide which they need. + int expectedCount = 8; + // Iterate through the input. + for (int degrees = 0; degrees < 361; degrees++) { + // Save the input-output mapping. + mSnapPrefer30sMap[degrees] = snappedOutputDegrees; + // If this is the last input for the specified output, calculate the next output and + // the next expected count. + if (count == expectedCount) { + snappedOutputDegrees += 6; + if (snappedOutputDegrees == 360) { + expectedCount = 7; + } else if (snappedOutputDegrees % 30 == 0) { + expectedCount = 14; + } else { + expectedCount = 4; + } + count = 1; + } else { + count++; + } + } + } + + /** + * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees, + * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be + * weighted heavier than the degrees corresponding to non-visible numbers. + * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the + * mapping. + */ + private int snapPrefer30s(int degrees) { + if (mSnapPrefer30sMap == null) { + return -1; + } + return mSnapPrefer30sMap[degrees]; + } + + /** + * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all + * multiples of 30), where the input will be "snapped" to the closest visible degrees. + * @param degrees The input degrees + * @param forceHigherOrLower The output may be forced to either the higher or lower step, or may + * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force + * strictly lower, and 0 to snap to the closer one. + * @return output degrees, will be a multiple of 30 + */ + private static int snapOnly30s(int degrees, int forceHigherOrLower) { + int stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE; + int floor = (degrees / stepSize) * stepSize; + int ceiling = floor + stepSize; + if (forceHigherOrLower == 1) { + degrees = ceiling; + } else if (forceHigherOrLower == -1) { + if (degrees == floor) { + floor -= stepSize; + } + degrees = floor; + } else { + if ((degrees - floor) < (ceiling - degrees)) { + degrees = floor; + } else { + degrees = ceiling; + } + } + return degrees; + } + + /** + * For the currently showing view (either hours or minutes), re-calculate the position for the + * selector, and redraw it at that position. The input degrees will be snapped to a selectable + * value. The text representing the currently selected value will be redrawn if required. + * @param degrees Degrees which should be selected. + * @param isInnerCircle Whether the selection should be in the inner circle; will be ignored + * if there is no inner circle. + * @param forceToVisibleValue Even if the currently-showing circle allows for fine-grained + * selection (i.e. minutes), force the selection to one of the visibly-showing values. + * @param forceDrawDot The dot in the circle will generally only be shown when the selection + * is on non-visible values, but use this to force the dot to be shown. + * @return The value that was selected, i.e. 0-23 for hours, 0-59 for minutes. + */ + private int reselectSelector(int degrees, boolean isInnerCircle, + boolean forceToVisibleValue, boolean forceDrawDot) { + if (degrees == -1) { + return -1; + } + int currentShowing = getCurrentItemShowing(); + + int stepSize; + boolean allowFineGrained = !forceToVisibleValue && (currentShowing == MINUTE_INDEX); + if (allowFineGrained) { + degrees = snapPrefer30s(degrees); + } else { + degrees = snapOnly30s(degrees, 0); + } + + RadialSelectorView radialSelectorView; + if (currentShowing == HOUR_INDEX) { + radialSelectorView = mHourRadialSelectorView; + stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE; + } else { + radialSelectorView = mMinuteRadialSelectorView; + stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE; + } + radialSelectorView.setSelection(degrees, isInnerCircle, forceDrawDot); + radialSelectorView.invalidate(); + + + if (currentShowing == HOUR_INDEX) { + if (mIs24HourMode) { + if (degrees == 0 && isInnerCircle) { + degrees = 360; + } else if (degrees == 360 && !isInnerCircle) { + degrees = 0; + } + } else if (degrees == 0) { + degrees = 360; + } + } else if (degrees == 360 && currentShowing == MINUTE_INDEX) { + degrees = 0; + } + + int value = degrees / stepSize; + + if (currentShowing == HOUR_INDEX && mIs24HourMode && !isInnerCircle && degrees != 0) { + value += 12; + } + + // Redraw the text if necessary + if(getCurrentItemShowing() == HOUR_INDEX) { + mHourRadialTextsView.setSelection(value); + mHourRadialTextsView.invalidate(); + } else if(getCurrentItemShowing() == MINUTE_INDEX) { + mMinuteRadialTextsView.setSelection(value); + mMinuteRadialTextsView.invalidate(); + } + + return value; + } + + /** + * Calculate the degrees within the circle that corresponds to the specified coordinates, if + * the coordinates are within the range that will trigger a selection. + * @param pointX The x coordinate. + * @param pointY The y coordinate. + * @param forceLegal Force the selection to be legal, regardless of how far the coordinates are + * from the actual numbers. + * @param isInnerCircle If the selection may be in the inner circle, pass in a size-1 boolean + * array here, inside which the value will be true if the selection is in the inner circle, + * and false if in the outer circle. + * @return Degrees from 0 to 360, if the selection was within the legal range. -1 if not. + */ + private int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal, + final Boolean[] isInnerCircle) { + int currentItem = getCurrentItemShowing(); + if (currentItem == HOUR_INDEX) { + return mHourRadialSelectorView.getDegreesFromCoords( + pointX, pointY, forceLegal, isInnerCircle); + } else if (currentItem == MINUTE_INDEX) { + return mMinuteRadialSelectorView.getDegreesFromCoords( + pointX, pointY, forceLegal, isInnerCircle); + } else { + return -1; + } + } + + /** + * Get the item (hours or minutes) that is currently showing. + */ + public int getCurrentItemShowing() { + if (mCurrentItemShowing != HOUR_INDEX && mCurrentItemShowing != MINUTE_INDEX) { + Log.e(TAG, "Current item showing was unfortunately set to "+mCurrentItemShowing); + return -1; + } + return mCurrentItemShowing; + } + + /** + * Set either minutes or hours as showing. + * @param animate True to animate the transition, false to show with no animation. + */ + public void setCurrentItemShowing(int index, boolean animate) { + if (index != HOUR_INDEX && index != MINUTE_INDEX) { + Log.e(TAG, "TimePicker does not support view at index "+index); + return; + } + + int lastIndex = getCurrentItemShowing(); + mCurrentItemShowing = index; + + if (animate && (index != lastIndex)) { + ObjectAnimator[] anims = new ObjectAnimator[4]; + if (index == MINUTE_INDEX) { + anims[0] = mHourRadialTextsView.getDisappearAnimator(); + anims[1] = mHourRadialSelectorView.getDisappearAnimator(); + anims[2] = mMinuteRadialTextsView.getReappearAnimator(); + anims[3] = mMinuteRadialSelectorView.getReappearAnimator(); + } else if (index == HOUR_INDEX){ + anims[0] = mHourRadialTextsView.getReappearAnimator(); + anims[1] = mHourRadialSelectorView.getReappearAnimator(); + anims[2] = mMinuteRadialTextsView.getDisappearAnimator(); + anims[3] = mMinuteRadialSelectorView.getDisappearAnimator(); + } + + if (mTransition != null && mTransition.isRunning()) { + mTransition.end(); + } + mTransition = new AnimatorSet(); + mTransition.playTogether(anims); + mTransition.start(); + } else { + int hourAlpha = (index == HOUR_INDEX) ? 255 : 0; + int minuteAlpha = (index == MINUTE_INDEX) ? 255 : 0; + mHourRadialTextsView.setAlpha(hourAlpha); + mHourRadialSelectorView.setAlpha(hourAlpha); + mMinuteRadialTextsView.setAlpha(minuteAlpha); + mMinuteRadialSelectorView.setAlpha(minuteAlpha); + } + + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + final float eventX = event.getX(); + final float eventY = event.getY(); + int degrees; + int value; + final Boolean[] isInnerCircle = new Boolean[1]; + isInnerCircle[0] = false; + + switch(event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (!mInputEnabled) { + return true; + } + + mDownX = eventX; + mDownY = eventY; + + mLastValueSelected = -1; + mDoingMove = false; + mDoingTouch = true; + // If we're showing the AM/PM, check to see if the user is touching it. + if (!mHideAmPm) { + mIsTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY); + } else { + mIsTouchingAmOrPm = -1; + } + if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) { + // If the touch is on AM or PM, set it as "touched" after the TAP_TIMEOUT + // in case the user moves their finger quickly. + mHapticFeedbackController.tryVibrate(); + mDownDegrees = -1; + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + mAmPmCirclesView.setAmOrPmPressed(mIsTouchingAmOrPm); + mAmPmCirclesView.invalidate(); + } + }, TAP_TIMEOUT); + } else { + // If we're in accessibility mode, force the touch to be legal. Otherwise, + // it will only register within the given touch target zone. + boolean forceLegal = mAccessibilityManager.isTouchExplorationEnabled(); + // Calculate the degrees that is currently being touched. + mDownDegrees = getDegreesFromCoords(eventX, eventY, forceLegal, isInnerCircle); + if (mDownDegrees != -1) { + // If it's a legal touch, set that number as "selected" after the + // TAP_TIMEOUT in case the user moves their finger quickly. + mHapticFeedbackController.tryVibrate(); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + mDoingMove = true; + int value = reselectSelector(mDownDegrees, isInnerCircle[0], + false, true); + mLastValueSelected = value; + mListener.onValueSelected(getCurrentItemShowing(), value, false); + } + }, TAP_TIMEOUT); + } + } + return true; + case MotionEvent.ACTION_MOVE: + if (!mInputEnabled) { + // We shouldn't be in this state, because input is disabled. + Log.e(TAG, "Input was disabled, but received ACTION_MOVE."); + return true; + } + + float dY = Math.abs(eventY - mDownY); + float dX = Math.abs(eventX - mDownX); + + if (!mDoingMove && dX <= TOUCH_SLOP && dY <= TOUCH_SLOP) { + // Hasn't registered down yet, just slight, accidental movement of finger. + break; + } + + // If we're in the middle of touching down on AM or PM, check if we still are. + // If so, no-op. If not, remove its pressed state. Either way, no need to check + // for touches on the other circle. + if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) { + mHandler.removeCallbacksAndMessages(null); + int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY); + if (isTouchingAmOrPm != mIsTouchingAmOrPm) { + mAmPmCirclesView.setAmOrPmPressed(-1); + mAmPmCirclesView.invalidate(); + mIsTouchingAmOrPm = -1; + } + break; + } + + if (mDownDegrees == -1) { + // Original down was illegal, so no movement will register. + break; + } + + // We're doing a move along the circle, so move the selection as appropriate. + mDoingMove = true; + mHandler.removeCallbacksAndMessages(null); + degrees = getDegreesFromCoords(eventX, eventY, true, isInnerCircle); + if (degrees != -1) { + value = reselectSelector(degrees, isInnerCircle[0], false, true); + if (value != mLastValueSelected) { + mHapticFeedbackController.tryVibrate(); + mLastValueSelected = value; + mListener.onValueSelected(getCurrentItemShowing(), value, false); + } + } + return true; + case MotionEvent.ACTION_UP: + if (!mInputEnabled) { + // If our touch input was disabled, tell the listener to re-enable us. + Log.d(TAG, "Input was disabled, but received ACTION_UP."); + mListener.onValueSelected(ENABLE_PICKER_INDEX, 1, false); + return true; + } + + mHandler.removeCallbacksAndMessages(null); + mDoingTouch = false; + + // If we're touching AM or PM, set it as selected, and tell the listener. + if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) { + int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY); + mAmPmCirclesView.setAmOrPmPressed(-1); + mAmPmCirclesView.invalidate(); + + if (isTouchingAmOrPm == mIsTouchingAmOrPm) { + mAmPmCirclesView.setAmOrPm(isTouchingAmOrPm); + if (getIsCurrentlyAmOrPm() != isTouchingAmOrPm) { + mListener.onValueSelected(AMPM_INDEX, mIsTouchingAmOrPm, false); + setValueForItem(AMPM_INDEX, isTouchingAmOrPm); + } + } + mIsTouchingAmOrPm = -1; + break; + } + + // If we have a legal degrees selected, set the value and tell the listener. + if (mDownDegrees != -1) { + degrees = getDegreesFromCoords(eventX, eventY, mDoingMove, isInnerCircle); + if (degrees != -1) { + value = reselectSelector(degrees, isInnerCircle[0], !mDoingMove, false); + + if (getCurrentItemShowing() == HOUR_INDEX && !mIs24HourMode) { + int amOrPm = getIsCurrentlyAmOrPm(); + if (amOrPm == AM && value == 12) { + value = 0; + } else if (amOrPm == PM && value != 12) { + value += 12; + } + } + setValueForItem(getCurrentItemShowing(), value); + mListener.onValueSelected(getCurrentItemShowing(), value, true); + } + } + mDoingMove = false; + return true; + default: + break; + } + return false; + } + + /** + * Set touch input as enabled or disabled, for use with keyboard mode. + */ + public boolean trySettingInputEnabled(boolean inputEnabled) { + if (mDoingTouch && !inputEnabled) { + // If we're trying to disable input, but we're in the middle of a touch event, + // we'll allow the touch event to continue before disabling input. + return false; + } + + mInputEnabled = inputEnabled; + mGrayBox.setVisibility(inputEnabled? View.INVISIBLE : View.VISIBLE); + return true; + } + + /** + * Necessary for accessibility, to ensure we support "scrolling" forward and backward + * in the circle. + */ + @Override + @SuppressWarnings("deprecation") + public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + if(Build.VERSION.SDK_INT >= 21) { + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); + } + else { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + + /** + * Announce the currently-selected time when launched. + */ + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + // Clear the event's current text so that only the current time will be spoken. + event.getText().clear(); + Calendar time = Calendar.getInstance(); + time.set(Calendar.HOUR, getHours()); + time.set(Calendar.MINUTE, getMinutes()); + long millis = time.getTimeInMillis(); + int flags = DateUtils.FORMAT_SHOW_TIME; + if (mIs24HourMode) { + flags |= DateUtils.FORMAT_24HOUR; + } + String timeString = LanguageUtils.getPersianNumbers( + DateUtils.formatDateTime(getContext(), millis, flags)); //TODO: Changed Here. + event.getText().add(timeString); + return true; + } + return super.dispatchPopulateAccessibilityEvent(event); + } + + /** + * When scroll forward/backward events are received, jump the time to the higher/lower + * discrete, visible value on the circle. + */ + @SuppressLint("NewApi") + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (super.performAccessibilityAction(action, arguments)) { + return true; + } + + int changeMultiplier = 0; + if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { + changeMultiplier = 1; + } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + changeMultiplier = -1; + } + if (changeMultiplier != 0) { + int value = getCurrentlyShowingValue(); + int stepSize = 0; + int currentItemShowing = getCurrentItemShowing(); + if (currentItemShowing == HOUR_INDEX) { + stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE; + value %= 12; + } else if (currentItemShowing == MINUTE_INDEX) { + stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE; + } + + int degrees = value * stepSize; + degrees = snapOnly30s(degrees, changeMultiplier); + value = degrees / stepSize; + int maxValue ; + int minValue = 0; + if (currentItemShowing == HOUR_INDEX) { + if (mIs24HourMode) { + maxValue = 23; + } else { + maxValue = 12; + minValue = 1; + } + } else { + maxValue = 55; + } + if (value > maxValue) { + // If we scrolled forward past the highest number, wrap around to the lowest. + value = minValue; + } else if (value < minValue) { + // If we scrolled backward past the lowest number, wrap around to the highest. + value = maxValue; + } + setItem(currentItemShowing, value); + mListener.onValueSelected(currentItemShowing, value, false); + return true; + } + + return false; + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/RadialSelectorView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/RadialSelectorView.java new file mode 100644 index 0000000..f787ab6 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/RadialSelectorView.java @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.time; + +import android.animation.Keyframe; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.Log; +import android.view.View; + +import mohammadaminha.com.widgets.Date_Picker.Utils; +import mohammadaminha.com.widgets.R; + +/** + * View to show what number is selected. This will draw a blue circle over the number, with a blue + * line coming from the center of the main circle to the edge of the blue selection. + */ +public class RadialSelectorView extends View { + private static final String TAG = "RadialSelectorView"; + + // Alpha level for selected circle. + private static final int SELECTED_ALPHA = Utils.SELECTED_ALPHA; + private static final int SELECTED_ALPHA_THEME_DARK = Utils.SELECTED_ALPHA_THEME_DARK; + // Alpha level for the line. + private static final int FULL_ALPHA = Utils.FULL_ALPHA; + + private Paint mPaint = new Paint(); + + private boolean mIsInitialized; + private boolean mDrawValuesReady; + + private float mCircleRadiusMultiplier; + private float mAmPmCircleRadiusMultiplier; + private float mInnerNumbersRadiusMultiplier; + private float mOuterNumbersRadiusMultiplier; + private float mNumbersRadiusMultiplier; + private float mSelectionRadiusMultiplier; + private float mAnimationRadiusMultiplier; + private boolean mIs24HourMode; + private boolean mHasInnerCircle; + private int mSelectionAlpha; + + private int mXCenter; + private int mYCenter; + private int mCircleRadius; + private float mTransitionMidRadiusMultiplier; + private float mTransitionEndRadiusMultiplier; + private int mLineLength; + private int mSelectionRadius; + private InvalidateUpdateListener mInvalidateUpdateListener; + + private int mSelectionDegrees; + private double mSelectionRadians; + private boolean mForceDrawDot; + + public RadialSelectorView(Context context) { + super(context); + mIsInitialized = false; + } + + /** + * Initialize this selector with the state of the picker. + * @param context Current context. + * @param is24HourMode Whether the selector is in 24-hour mode, which will tell us + * whether the circle's center is moved up slightly to make room for the AM/PM circles. + * @param hasInnerCircle Whether we have both an inner and an outer circle of numbers + * that may be selected. Should be true for 24-hour mode in the hours circle. + * @param disappearsOut Whether the numbers' animation will have them disappearing out + * or disappearing in. + * @param selectionDegrees The initial degrees to be selected. + * @param isInnerCircle Whether the initial selection is in the inner or outer circle. + * Will be ignored when hasInnerCircle is false. + */ + public void initialize(Context context, boolean is24HourMode, boolean hasInnerCircle, + boolean disappearsOut, int selectionDegrees, boolean isInnerCircle) { + if (mIsInitialized) { + Log.e(TAG, "This RadialSelectorView may only be initialized once."); + return; + } + + Resources res = context.getResources(); + + int accentColor = res.getColor(R.color.mdtp_accent_color); + mPaint.setColor(accentColor); + mPaint.setAntiAlias(true); + mSelectionAlpha = SELECTED_ALPHA; + + // Calculate values for the circle radius size. + mIs24HourMode = is24HourMode; + if (is24HourMode) { + mCircleRadiusMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_circle_radius_multiplier_24HourMode)); + } else { + mCircleRadiusMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_circle_radius_multiplier)); + mAmPmCircleRadiusMultiplier = + Float.parseFloat(res.getString(R.string.mdtp_ampm_circle_radius_multiplier)); + } + + // Calculate values for the radius size(s) of the numbers circle(s). + mHasInnerCircle = hasInnerCircle; + if (hasInnerCircle) { + mInnerNumbersRadiusMultiplier = + Float.parseFloat(res.getString(R.string.mdtp_numbers_radius_multiplier_inner)); + mOuterNumbersRadiusMultiplier = + Float.parseFloat(res.getString(R.string.mdtp_numbers_radius_multiplier_outer)); + } else { + mNumbersRadiusMultiplier = + Float.parseFloat(res.getString(R.string.mdtp_numbers_radius_multiplier_normal)); + } + mSelectionRadiusMultiplier = + Float.parseFloat(res.getString(R.string.mdtp_selection_radius_multiplier)); + + // Calculate values for the transition mid-way states. + mAnimationRadiusMultiplier = 1; + mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut? -1 : 1)); + mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut? 1 : -1)); + mInvalidateUpdateListener = new InvalidateUpdateListener(); + + setSelection(selectionDegrees, isInnerCircle, false); + mIsInitialized = true; + } + + /* package */ void setTheme(Context context, boolean themeDark) { + Resources res = context.getResources(); + int color; + if (themeDark) { + color = res.getColor(R.color.mdtp_red); + mSelectionAlpha = SELECTED_ALPHA_THEME_DARK; + } else { + color = res.getColor(R.color.mdtp_accent_color); + mSelectionAlpha = SELECTED_ALPHA; + } + mPaint.setColor(color); + } + + /** + * Set the selection. + * @param selectionDegrees The degrees to be selected. + * @param isInnerCircle Whether the selection should be in the inner circle or outer. Will be + * ignored if hasInnerCircle was initialized to false. + * @param forceDrawDot Whether to force the dot in the center of the selection circle to be + * drawn. If false, the dot will be drawn only when the degrees is not a multiple of 30, i.e. + * the selection is not on a visible number. + */ + public void setSelection(int selectionDegrees, boolean isInnerCircle, boolean forceDrawDot) { + mSelectionDegrees = selectionDegrees; + mSelectionRadians = selectionDegrees * Math.PI / 180; + mForceDrawDot = forceDrawDot; + + if (mHasInnerCircle) { + if (isInnerCircle) { + mNumbersRadiusMultiplier = mInnerNumbersRadiusMultiplier; + } else { + mNumbersRadiusMultiplier = mOuterNumbersRadiusMultiplier; + } + } + } + + /** + * Allows for smoother animations. + */ + @Override + public boolean hasOverlappingRendering() { + return false; + } + + /** + * Set the multiplier for the radius. Will be used during animations to move in/out. + */ + public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) { + mAnimationRadiusMultiplier = animationRadiusMultiplier; + } + + public int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal, + final Boolean[] isInnerCircle) { + if (!mDrawValuesReady) { + return -1; + } + + double hypotenuse = Math.sqrt( + (pointY - mYCenter)*(pointY - mYCenter) + + (pointX - mXCenter)*(pointX - mXCenter)); + // Check if we're outside the range + if (mHasInnerCircle) { + if (forceLegal) { + // If we're told to force the coordinates to be legal, we'll set the isInnerCircle + // boolean based based off whichever number the coordinates are closer to. + int innerNumberRadius = (int) (mCircleRadius * mInnerNumbersRadiusMultiplier); + int distanceToInnerNumber = (int) Math.abs(hypotenuse - innerNumberRadius); + int outerNumberRadius = (int) (mCircleRadius * mOuterNumbersRadiusMultiplier); + int distanceToOuterNumber = (int) Math.abs(hypotenuse - outerNumberRadius); + + isInnerCircle[0] = (distanceToInnerNumber <= distanceToOuterNumber); + } else { + // Otherwise, if we're close enough to either number (with the space between the + // two allotted equally), set the isInnerCircle boolean as the closer one. + // appropriately, but otherwise return -1. + int minAllowedHypotenuseForInnerNumber = + (int) (mCircleRadius * mInnerNumbersRadiusMultiplier) - mSelectionRadius; + int maxAllowedHypotenuseForOuterNumber = + (int) (mCircleRadius * mOuterNumbersRadiusMultiplier) + mSelectionRadius; + int halfwayHypotenusePoint = (int) (mCircleRadius * + ((mOuterNumbersRadiusMultiplier + mInnerNumbersRadiusMultiplier) / 2)); + + if (hypotenuse >= minAllowedHypotenuseForInnerNumber && + hypotenuse <= halfwayHypotenusePoint) { + isInnerCircle[0] = true; + } else if (hypotenuse <= maxAllowedHypotenuseForOuterNumber && + hypotenuse >= halfwayHypotenusePoint) { + isInnerCircle[0] = false; + } else { + return -1; + } + } + } else { + // If there's just one circle, we'll need to return -1 if: + // we're not told to force the coordinates to be legal, and + // the coordinates' distance to the number is within the allowed distance. + if (!forceLegal) { + int distanceToNumber = (int) Math.abs(hypotenuse - mLineLength); + // The max allowed distance will be defined as the distance from the center of the + // number to the edge of the circle. + int maxAllowedDistance = (int) (mCircleRadius * (1 - mNumbersRadiusMultiplier)); + if (distanceToNumber > maxAllowedDistance) { + return -1; + } + } + } + + + float opposite = Math.abs(pointY - mYCenter); + double radians = Math.asin(opposite / hypotenuse); + int degrees = (int) (radians * 180 / Math.PI); + + // Now we have to translate to the correct quadrant. + boolean rightSide = (pointX > mXCenter); + boolean topSide = (pointY < mYCenter); + if (rightSide && topSide) { + degrees = 90 - degrees; + } else if (rightSide && !topSide) { + degrees = 90 + degrees; + } else if (!rightSide && !topSide) { + degrees = 270 - degrees; + } else if (!rightSide && topSide) { + degrees = 270 + degrees; + } + return degrees; + } + + @Override + public void onDraw(Canvas canvas) { + int viewWidth = getWidth(); + if (viewWidth == 0 || !mIsInitialized) { + return; + } + + if (!mDrawValuesReady) { + mXCenter = getWidth() / 2; + mYCenter = getHeight() / 2; + mCircleRadius = (int) (Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier); + + if (!mIs24HourMode) { + // We'll need to draw the AM/PM circles, so the main circle will need to have + // a slightly higher center. To keep the entire view centered vertically, we'll + // have to push it up by half the radius of the AM/PM circles. + int amPmCircleRadius = (int) (mCircleRadius * mAmPmCircleRadiusMultiplier); + mYCenter -= amPmCircleRadius *0.75; + } + + mSelectionRadius = (int) (mCircleRadius * mSelectionRadiusMultiplier); + + mDrawValuesReady = true; + } + + // Calculate the current radius at which to place the selection circle. + mLineLength = (int) (mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier); + int pointX = mXCenter + (int) (mLineLength * Math.sin(mSelectionRadians)); + int pointY = mYCenter - (int) (mLineLength * Math.cos(mSelectionRadians)); + + // Draw the selection circle. + mPaint.setAlpha(mSelectionAlpha); + canvas.drawCircle(pointX, pointY, mSelectionRadius, mPaint); + + if (mForceDrawDot | mSelectionDegrees % 30 != 0) { + // We're not on a direct tick (or we've been told to draw the dot anyway). + mPaint.setAlpha(FULL_ALPHA); + canvas.drawCircle(pointX, pointY, (mSelectionRadius * 2 / 7), mPaint); + } else { + // We're not drawing the dot, so shorten the line to only go as far as the edge of the + // selection circle. + int lineLength = mLineLength; + lineLength -= mSelectionRadius; + pointX = mXCenter + (int) (lineLength * Math.sin(mSelectionRadians)); + pointY = mYCenter - (int) (lineLength * Math.cos(mSelectionRadians)); + } + + // Draw the line from the center of the circle. + mPaint.setAlpha(255); + mPaint.setStrokeWidth(1); + canvas.drawLine(mXCenter, mYCenter, pointX, pointY, mPaint); + } + + public ObjectAnimator getDisappearAnimator() { + if (!mIsInitialized || !mDrawValuesReady) { + Log.e(TAG, "RadialSelectorView was not ready for animation."); + return null; + } + + Keyframe kf0, kf1, kf2; + float midwayPoint = 0.2f; + int duration = 500; + + kf0 = Keyframe.ofFloat(0f, 1); + kf1 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier); + kf2 = Keyframe.ofFloat(1f, mTransitionEndRadiusMultiplier); + PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe( + "animationRadiusMultiplier", kf0, kf1, kf2); + + kf0 = Keyframe.ofFloat(0f, 1f); + kf1 = Keyframe.ofFloat(1f, 0f); + PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1); + + ObjectAnimator disappearAnimator = ObjectAnimator.ofPropertyValuesHolder( + this, radiusDisappear, fadeOut).setDuration(duration); + disappearAnimator.addUpdateListener(mInvalidateUpdateListener); + + return disappearAnimator; + } + + public ObjectAnimator getReappearAnimator() { + if (!mIsInitialized || !mDrawValuesReady) { + Log.e(TAG, "RadialSelectorView was not ready for animation."); + return null; + } + + Keyframe kf0, kf1, kf2, kf3; + float midwayPoint = 0.2f; + int duration = 500; + + // The time points are half of what they would normally be, because this animation is + // staggered against the disappear so they happen seamlessly. The reappear starts + // halfway into the disappear. + float delayMultiplier = 0.25f; + float transitionDurationMultiplier = 1f; + float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; + int totalDuration = (int) (duration * totalDurationMultiplier); + float delayPoint = (delayMultiplier * duration) / totalDuration; + midwayPoint = 1 - (midwayPoint * (1 - delayPoint)); + + kf0 = Keyframe.ofFloat(0f, mTransitionEndRadiusMultiplier); + kf1 = Keyframe.ofFloat(delayPoint, mTransitionEndRadiusMultiplier); + kf2 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier); + kf3 = Keyframe.ofFloat(1f, 1); + PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe( + "animationRadiusMultiplier", kf0, kf1, kf2, kf3); + + kf0 = Keyframe.ofFloat(0f, 0f); + kf1 = Keyframe.ofFloat(delayPoint, 0f); + kf2 = Keyframe.ofFloat(1f, 1f); + PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2); + + ObjectAnimator reappearAnimator = ObjectAnimator.ofPropertyValuesHolder( + this, radiusReappear, fadeIn).setDuration(totalDuration); + reappearAnimator.addUpdateListener(mInvalidateUpdateListener); + return reappearAnimator; + } + + /** + * We'll need to invalidate during the animation. + */ + private class InvalidateUpdateListener implements AnimatorUpdateListener { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + RadialSelectorView.this.invalidate(); + } + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/RadialTextsView.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/RadialTextsView.java new file mode 100644 index 0000000..3d44951 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/RadialTextsView.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2013 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. + */ + +package mohammadaminha.com.widgets.Date_Picker.time; + +import android.animation.Keyframe; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Typeface; +import android.util.Log; +import android.view.View; + +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.R; +import mohammadaminha.com.widgets.Util; + +/** + * A view to show a series of numbers in a circular pattern. + */ +public class RadialTextsView extends View { + private static String TAG = "RadialTextsView"; + + private Paint mPaint = new Paint(); + private Paint mSelectedPaint = new Paint(); + + private boolean mDrawValuesReady; + private boolean mIsInitialized; + + private int selection = -1; + + private Typeface mTypefaceLight; + private Typeface mTypefaceRegular; + private String[] mTexts; + private String[] mInnerTexts; + private boolean mIs24HourMode; + private boolean mHasInnerCircle; + private float mCircleRadiusMultiplier; + private float mAmPmCircleRadiusMultiplier; + private float mNumbersRadiusMultiplier; + private float mInnerNumbersRadiusMultiplier; + private float mTextSizeMultiplier; + private float mInnerTextSizeMultiplier; + + private int mXCenter; + private int mYCenter; + private float mCircleRadius; + private boolean mTextGridValuesDirty; + private float mTextSize; + private float mInnerTextSize; + private float[] mTextGridHeights; + private float[] mTextGridWidths; + private float[] mInnerTextGridHeights; + private float[] mInnerTextGridWidths; + + private float mAnimationRadiusMultiplier; + private float mTransitionMidRadiusMultiplier; + private float mTransitionEndRadiusMultiplier; + private ObjectAnimator mDisappearAnimator; + private ObjectAnimator mReappearAnimator; + private InvalidateUpdateListener mInvalidateUpdateListener; + + public RadialTextsView(Context context) { + super(context); + mIsInitialized = false; + } + + public void initialize(Resources res, String[] texts, String[] innerTexts, + boolean is24HourMode, boolean disappearsOut) { + if (mIsInitialized) { + Log.e(TAG, "This RadialTextsView may only be initialized once."); + return; + } + + // Set up the paint. + int numbersTextColor = res.getColor(R.color.mdtp_numbers_text_color); + mPaint.setColor(numbersTextColor); + + mTypefaceLight = Util.getTypeFace(); + mTypefaceRegular = Util.getTypeFace(); + mPaint.setAntiAlias(true); + mPaint.setTextAlign(Align.CENTER); + + // Set up the selected paint + int selectedTextColor = res.getColor(R.color.YellowLight); + mSelectedPaint.setColor(selectedTextColor); + mSelectedPaint.setAntiAlias(true); + mSelectedPaint.setTextAlign(Align.CENTER); + + mTexts = texts; + mInnerTexts = innerTexts; + mIs24HourMode = is24HourMode; + mHasInnerCircle = (innerTexts != null); + + // Calculate the radius for the main circle. + if (is24HourMode) { + mCircleRadiusMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_circle_radius_multiplier_24HourMode)); + } else { + mCircleRadiusMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_circle_radius_multiplier)); + mAmPmCircleRadiusMultiplier = + Float.parseFloat(res.getString(R.string.mdtp_ampm_circle_radius_multiplier)); + } + + // Initialize the widths and heights of the grid, and calculate the values for the numbers. + mTextGridHeights = new float[7]; + mTextGridWidths = new float[7]; + if (mHasInnerCircle) { + mNumbersRadiusMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_numbers_radius_multiplier_outer)); + mTextSizeMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_text_size_multiplier_outer)); + mInnerNumbersRadiusMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_numbers_radius_multiplier_inner)); + mInnerTextSizeMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_text_size_multiplier_inner)); + + mInnerTextGridHeights = new float[7]; + mInnerTextGridWidths = new float[7]; + } else { + mNumbersRadiusMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_numbers_radius_multiplier_normal)); + mTextSizeMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_text_size_multiplier_normal)); + } + + mAnimationRadiusMultiplier = 1; + mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut ? -1 : 1)); + mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut ? 1 : -1)); + mInvalidateUpdateListener = new InvalidateUpdateListener(); + + mTextGridValuesDirty = true; + mIsInitialized = true; + } + + /* package */ void setTheme(Context context, boolean themeDark) { + Resources res = context.getResources(); + int textColor; + if (themeDark) { + textColor = res.getColor(R.color.YellowLight); + } else { + textColor = res.getColor(R.color.mdtp_numbers_text_color); + } + mPaint.setColor(textColor); + } + + /** + * Set the value of the selected text. Depending on the theme this will be rendered differently + * + * @param selection The text which is currently selected + */ + void setSelection(int selection) { + this.selection = selection; + } + + /** + * Allows for smoother animation. + */ + @Override + public boolean hasOverlappingRendering() { + return false; + } + + /** + * Used by the animation to move the numbers in and out. + */ + @SuppressWarnings("unused") + public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) { + mAnimationRadiusMultiplier = animationRadiusMultiplier; + mTextGridValuesDirty = true; + } + + @Override + public void onDraw(Canvas canvas) { + int viewWidth = getWidth(); + if (viewWidth == 0 || !mIsInitialized) { + return; + } + + if (!mDrawValuesReady) { + mXCenter = getWidth() / 2; + mYCenter = getHeight() / 2; + mCircleRadius = Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier; + if (!mIs24HourMode) { + // We'll need to draw the AM/PM circles, so the main circle will need to have + // a slightly higher center. To keep the entire view centered vertically, we'll + // have to push it up by half the radius of the AM/PM circles. + float amPmCircleRadius = mCircleRadius * mAmPmCircleRadiusMultiplier; + mYCenter -= amPmCircleRadius * 0.75; + } + + mTextSize = mCircleRadius * mTextSizeMultiplier; + if (mHasInnerCircle) { + mInnerTextSize = mCircleRadius * mInnerTextSizeMultiplier; + } + + // Because the text positions will be static, pre-render the animations. + renderAnimations(); + + mTextGridValuesDirty = true; + mDrawValuesReady = true; + } + + // Calculate the text positions, but only if they've changed since the last onDraw. + if (mTextGridValuesDirty) { + float numbersRadius = + mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier; + + // Calculate the positions for the 12 numbers in the main circle. + calculateGridSizes(numbersRadius, mXCenter, mYCenter, + mTextSize, mTextGridHeights, mTextGridWidths); + if (mHasInnerCircle) { + // If we have an inner circle, calculate those positions too. + float innerNumbersRadius = + mCircleRadius * mInnerNumbersRadiusMultiplier * mAnimationRadiusMultiplier; + calculateGridSizes(innerNumbersRadius, mXCenter, mYCenter, + mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths); + } + mTextGridValuesDirty = false; + } + + // Draw the texts in the pre-calculated positions. + drawTexts(canvas, mTextSize, mTypefaceLight, mTexts, mTextGridWidths, mTextGridHeights); + if (mHasInnerCircle) { + drawTexts(canvas, mInnerTextSize, mTypefaceRegular, mInnerTexts, + mInnerTextGridWidths, mInnerTextGridHeights); + } + } + + /** + * Using the trigonometric Unit Circle, calculate the positions that the text will need to be + * drawn at based on the specified circle radius. Place the values in the textGridHeights and + * textGridWidths parameters. + */ + private void calculateGridSizes(float numbersRadius, float xCenter, float yCenter, + float textSize, float[] textGridHeights, float[] textGridWidths) { + /* + * The numbers need to be drawn in a 7x7 grid, representing the points on the Unit Circle. + */ + // cos(30) = a / r => r * cos(30) = a => r * √3/2 = a + float offset2 = numbersRadius * ((float) Math.sqrt(3)) / 2f; + // sin(30) = o / r => r * sin(30) = o => r / 2 = a + float offset3 = numbersRadius / 2f; + mPaint.setTextSize(textSize); + mSelectedPaint.setTextSize(textSize); + // We'll need yTextBase to be slightly lower to account for the text's baseline. + yCenter -= (mPaint.descent() + mPaint.ascent()) / 2; + + textGridHeights[0] = yCenter - numbersRadius; + textGridWidths[0] = xCenter - numbersRadius; + textGridHeights[1] = yCenter - offset2; + textGridWidths[1] = xCenter - offset2; + textGridHeights[2] = yCenter - offset3; + textGridWidths[2] = xCenter - offset3; + textGridHeights[3] = yCenter; + textGridWidths[3] = xCenter; + textGridHeights[4] = yCenter + offset3; + textGridWidths[4] = xCenter + offset3; + textGridHeights[5] = yCenter + offset2; + textGridWidths[5] = xCenter + offset2; + textGridHeights[6] = yCenter + numbersRadius; + textGridWidths[6] = xCenter + numbersRadius; + } + + /** + * Draw the 12 text values at the positions specified by the textGrid parameters. + */ + private void drawTexts(Canvas canvas, float textSize, Typeface typeface, String[] texts, + float[] textGridWidths, float[] textGridHeights) { + mPaint.setTextSize(textSize); + mPaint.setTypeface(typeface); + LanguageUtils.getPersianNumbers(texts); + canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], Integer.parseInt(texts[0]) == selection ? mSelectedPaint : mPaint); + canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], Integer.parseInt(texts[1]) == selection ? mSelectedPaint : mPaint); + canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], Integer.parseInt(texts[2]) == selection ? mSelectedPaint : mPaint); + canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], Integer.parseInt(texts[3]) == selection ? mSelectedPaint : mPaint); + canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], Integer.parseInt(texts[4]) == selection ? mSelectedPaint : mPaint); + canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], Integer.parseInt(texts[5]) == selection ? mSelectedPaint : mPaint); + canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], Integer.parseInt(texts[6]) == selection ? mSelectedPaint : mPaint); + canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], Integer.parseInt(texts[7]) == selection ? mSelectedPaint : mPaint); + canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], Integer.parseInt(texts[8]) == selection ? mSelectedPaint : mPaint); + canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], Integer.parseInt(texts[9]) == selection ? mSelectedPaint : mPaint); + canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], Integer.parseInt(texts[10]) == selection ? mSelectedPaint : mPaint); + canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], Integer.parseInt(texts[11]) == selection ? mSelectedPaint : mPaint); + } + + /** + * Render the animations for appearing and disappearing. + */ + private void renderAnimations() { + Keyframe kf0, kf1, kf2, kf3; + float midwayPoint = 0.2f; + int duration = 500; + + // Set up animator for disappearing. + kf0 = Keyframe.ofFloat(0f, 1); + kf1 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier); + kf2 = Keyframe.ofFloat(1f, mTransitionEndRadiusMultiplier); + PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe( + "animationRadiusMultiplier", kf0, kf1, kf2); + + kf0 = Keyframe.ofFloat(0f, 1f); + kf1 = Keyframe.ofFloat(1f, 0f); + PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1); + + mDisappearAnimator = ObjectAnimator.ofPropertyValuesHolder( + this, radiusDisappear, fadeOut).setDuration(duration); + mDisappearAnimator.addUpdateListener(mInvalidateUpdateListener); + + + // Set up animator for reappearing. + float delayMultiplier = 0.25f; + float transitionDurationMultiplier = 1f; + float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; + int totalDuration = (int) (duration * totalDurationMultiplier); + float delayPoint = (delayMultiplier * duration) / totalDuration; + midwayPoint = 1 - (midwayPoint * (1 - delayPoint)); + + kf0 = Keyframe.ofFloat(0f, mTransitionEndRadiusMultiplier); + kf1 = Keyframe.ofFloat(delayPoint, mTransitionEndRadiusMultiplier); + kf2 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier); + kf3 = Keyframe.ofFloat(1f, 1); + PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe( + "animationRadiusMultiplier", kf0, kf1, kf2, kf3); + + kf0 = Keyframe.ofFloat(0f, 0f); + kf1 = Keyframe.ofFloat(delayPoint, 0f); + kf2 = Keyframe.ofFloat(1f, 1f); + PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2); + + mReappearAnimator = ObjectAnimator.ofPropertyValuesHolder( + this, radiusReappear, fadeIn).setDuration(totalDuration); + mReappearAnimator.addUpdateListener(mInvalidateUpdateListener); + } + + public ObjectAnimator getDisappearAnimator() { + if (!mIsInitialized || !mDrawValuesReady || mDisappearAnimator == null) { + Log.e(TAG, "RadialTextView was not ready for animation."); + return null; + } + + return mDisappearAnimator; + } + + public ObjectAnimator getReappearAnimator() { + if (!mIsInitialized || !mDrawValuesReady || mReappearAnimator == null) { + Log.e(TAG, "RadialTextView was not ready for animation."); + return null; + } + + return mReappearAnimator; + } + + private class InvalidateUpdateListener implements AnimatorUpdateListener { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + RadialTextsView.this.invalidate(); + } + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/TimePickerDialog.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/TimePickerDialog.java new file mode 100644 index 0000000..c0ac49d --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/time/TimePickerDialog.java @@ -0,0 +1,1046 @@ +/* + * Copyright (C) 2013 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 + */ + +package mohammadaminha.com.widgets.Date_Picker.time; + +import android.animation.ObjectAnimator; +import android.app.ActionBar.LayoutParams; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.util.Log; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnKeyListener; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.RelativeLayout; + +import java.util.ArrayList; +import java.util.Locale; + +import mohammadaminha.com.widgets.Button; +import mohammadaminha.com.widgets.Date_Picker.HapticFeedbackController; +import mohammadaminha.com.widgets.Date_Picker.TypefaceHelper; +import mohammadaminha.com.widgets.Date_Picker.Utils; +import mohammadaminha.com.widgets.Date_Picker.time.RadialPickerLayout.OnValueSelectedListener; +import mohammadaminha.com.widgets.Date_Picker.utils.LanguageUtils; +import mohammadaminha.com.widgets.R; +import mohammadaminha.com.widgets.TextView; + +/** + * Dialog to set a time. + */ +public class TimePickerDialog extends DialogFragment implements OnValueSelectedListener{ + private static final String TAG = "TimePickerDialog"; + + private static final String KEY_HOUR_OF_DAY = "hour_of_day"; + private static final String KEY_MINUTE = "minute"; + private static final String KEY_IS_24_HOUR_VIEW = "is_24_hour_view"; + private static final String KEY_TITLE = "dialog_title"; + private static final String KEY_CURRENT_ITEM_SHOWING = "current_item_showing"; + private static final String KEY_IN_KB_MODE = "in_kb_mode"; + private static final String KEY_TYPED_TIMES = "typed_times"; + private static final String KEY_DARK_THEME = "dark_theme"; + + public static final int HOUR_INDEX = 0; + public static final int MINUTE_INDEX = 1; + // NOT a real index for the purpose of what's showing. + public static final int AMPM_INDEX = 2; + // Also NOT a real index, just used for keyboard mode. + public static final int ENABLE_PICKER_INDEX = 3; + public static final int AM = 0; + public static final int PM = 1; + + // Delay before starting the pulse animation, in ms. + private static final int PULSE_ANIMATOR_DELAY = 300; + + private OnTimeSetListener mCallback; + private DialogInterface.OnCancelListener mOnCancelListener; + private DialogInterface.OnDismissListener mOnDismissListener; + + private HapticFeedbackController mHapticFeedbackController; + + private Button mOkButton; + private TextView mHourView; + private TextView mHourSpaceView; + private TextView mMinuteView; + private TextView mMinuteSpaceView; + private TextView mAmPmTextView; + private View mAmPmHitspace; + private RadialPickerLayout mTimePicker; + + private int mSelectedColor; + private int mUnselectedColor; + private String mAmText; + private String mPmText; + + private boolean mAllowAutoAdvance; + private int mInitialHourOfDay; + private int mInitialMinute; + private boolean mIs24HourMode; + private String mTitle; + private boolean mThemeDark; + + // For hardware IME input. + private char mPlaceholderText; + private String mDoublePlaceholderText; + private String mDeletedKeyFormat; + private boolean mInKbMode; + private ArrayList mTypedTimes; + private Node mLegalTimesTree; + private int mAmKeyCode; + private int mPmKeyCode; + + // Accessibility strings. + private String mHourPickerDescription; + private String mSelectHours; + private String mMinutePickerDescription; + private String mSelectMinutes; + + /** + * The callback interface used to indicate the user is done filling in + * the time (they clicked on the 'Set' button). + */ + public interface OnTimeSetListener { + + /** + * @param view The view associated with this listener. + * @param hourOfDay The hour that was set. + * @param minute The minute that was set. + */ + void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute); + } + + public TimePickerDialog() { + // Empty constructor required for dialog fragment. + } + + /** + public TimePickerDialog(Context context, int theme, OnTimeSetListener callback, + int hourOfDay, int minute, boolean is24HourMode) { + // Empty constructor required for dialog fragment. + } + **/ + + public static TimePickerDialog newInstance(OnTimeSetListener callback, + int hourOfDay, int minute, boolean is24HourMode) { + TimePickerDialog ret = new TimePickerDialog(); + ret.initialize(callback, hourOfDay, minute, is24HourMode); + return ret; + } + + private void initialize(OnTimeSetListener callback, + int hourOfDay, int minute, boolean is24HourMode) { + mCallback = callback; + + mInitialHourOfDay = hourOfDay; + mInitialMinute = minute; + mIs24HourMode = is24HourMode; + mInKbMode = false; + mTitle = ""; + mThemeDark = false; + } + + /** + * Set a title. NOTE: this will only take effect with the next onCreateView + */ + public void setTitle(String title) { + mTitle = title; + } + + public String getTitle() { + return mTitle; + } + + /** + * Set a dark or light theme. NOTE: this will only take effect for the next onCreateView. + */ + public void setThemeDark(boolean dark) { + mThemeDark = dark; + } + + public boolean isThemeDark() { + return mThemeDark; + } + + public void setOnTimeSetListener(OnTimeSetListener callback) { + mCallback = callback; + } + + public void setOnCancelListener(DialogInterface.OnCancelListener onCancelListener) { + mOnCancelListener = onCancelListener; + } + + public void setOnDismissListener(DialogInterface.OnDismissListener onDismissListener) { + mOnDismissListener = onDismissListener; + } + + public void setStartTime(int hourOfDay, int minute) { + mInitialHourOfDay = hourOfDay; + mInitialMinute = minute; + mInKbMode = false; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null && savedInstanceState.containsKey(KEY_HOUR_OF_DAY) + && savedInstanceState.containsKey(KEY_MINUTE) + && savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) { + mInitialHourOfDay = savedInstanceState.getInt(KEY_HOUR_OF_DAY); + mInitialMinute = savedInstanceState.getInt(KEY_MINUTE); + mIs24HourMode = savedInstanceState.getBoolean(KEY_IS_24_HOUR_VIEW); + mInKbMode = savedInstanceState.getBoolean(KEY_IN_KB_MODE); + mTitle = savedInstanceState.getString(KEY_TITLE); + mThemeDark = savedInstanceState.getBoolean(KEY_DARK_THEME); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE); + + View view = inflater.inflate(R.layout.mdtp_time_picker_dialog, null); + KeyboardListener keyboardListener = new KeyboardListener(); + view.findViewById(R.id.time_picker_dialog).setOnKeyListener(keyboardListener); + + Resources res = getResources(); + mHourPickerDescription = res.getString(R.string.mdtp_hour_picker_description); + mSelectHours = res.getString(R.string.mdtp_select_hours); + mMinutePickerDescription = res.getString(R.string.mdtp_minute_picker_description); + mSelectMinutes = res.getString(R.string.mdtp_select_minutes); + mSelectedColor = res.getColor(R.color.mdtp_white); + mUnselectedColor = res.getColor(R.color.mdtp_white); + + mHourView = view.findViewById(R.id.hours); + mHourView.setOnKeyListener(keyboardListener); + mHourSpaceView = view.findViewById(R.id.hour_space); + mMinuteSpaceView = view.findViewById(R.id.minutes_space); + mMinuteView = view.findViewById(R.id.minutes); + mMinuteView.setOnKeyListener(keyboardListener); + mAmPmTextView = view.findViewById(R.id.ampm_label); + mAmPmTextView.setOnKeyListener(keyboardListener); + mAmText = "قبل‌ازظهر"; + mPmText = "بعدازظهر"; + + mHapticFeedbackController = new HapticFeedbackController(getActivity()); + + mTimePicker = view.findViewById(R.id.time_picker); + mTimePicker.setOnValueSelectedListener(this); + mTimePicker.setOnKeyListener(keyboardListener); + mTimePicker.initialize(getActivity(), mHapticFeedbackController, mInitialHourOfDay, + mInitialMinute, mIs24HourMode); + + int currentItemShowing = HOUR_INDEX; + if (savedInstanceState != null && + savedInstanceState.containsKey(KEY_CURRENT_ITEM_SHOWING)) { + currentItemShowing = savedInstanceState.getInt(KEY_CURRENT_ITEM_SHOWING); + } + setCurrentItemShowing(currentItemShowing, false, true, true); + mTimePicker.invalidate(); + + mHourView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + setCurrentItemShowing(HOUR_INDEX, true, false, true); + tryVibrate(); + } + }); + mMinuteView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + setCurrentItemShowing(MINUTE_INDEX, true, false, true); + tryVibrate(); + } + }); + + mOkButton = view.findViewById(R.id.ok); + mOkButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mInKbMode && isTypedTimeFullyLegal()) { + finishKbMode(false); + } else { + tryVibrate(); + } + if (mCallback != null) { + mCallback.onTimeSet(mTimePicker, + mTimePicker.getHours(), mTimePicker.getMinutes()); + } + dismiss(); + } + }); + mOkButton.setOnKeyListener(keyboardListener); + mOkButton.setTypeface(TypefaceHelper.get(getDialog().getContext(),"Roboto-Medium")); + + Button mCancelButton = view.findViewById(R.id.cancel); + mCancelButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + tryVibrate(); + getDialog().cancel(); + } + }); + mCancelButton.setTypeface(TypefaceHelper.get(getDialog().getContext(),"Roboto-Medium")); + mCancelButton.setVisibility(isCancelable() ? View.VISIBLE : View.GONE); + + // Enable or disable the AM/PM view. + mAmPmHitspace = view.findViewById(R.id.ampm_hitspace); + if (mIs24HourMode) { + mAmPmTextView.setVisibility(View.GONE); + + RelativeLayout.LayoutParams paramsSeparator = new RelativeLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + paramsSeparator.addRule(RelativeLayout.CENTER_IN_PARENT); + TextView separatorView = view.findViewById(R.id.separator); + separatorView.setLayoutParams(paramsSeparator); + } else { + mAmPmTextView.setVisibility(View.VISIBLE); + updateAmPmDisplay(mInitialHourOfDay < 12? AM : PM); + mAmPmHitspace.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + tryVibrate(); + int amOrPm = mTimePicker.getIsCurrentlyAmOrPm(); + if (amOrPm == AM) { + amOrPm = PM; + } else if (amOrPm == PM){ + amOrPm = AM; + } + updateAmPmDisplay(amOrPm); + mTimePicker.setAmOrPm(amOrPm); + } + }); + } + + mAllowAutoAdvance = true; + setHour(mInitialHourOfDay, true); + setMinute(mInitialMinute); + + // Set up for keyboard mode. + mDoublePlaceholderText = res.getString(R.string.mdtp_time_placeholder); + mDeletedKeyFormat = res.getString(R.string.mdtp_deleted_key); + mPlaceholderText = mDoublePlaceholderText.charAt(0); + mAmKeyCode = mPmKeyCode = -1; + generateLegalTimesTree(); + if (mInKbMode) { + mTypedTimes = savedInstanceState.getIntegerArrayList(KEY_TYPED_TIMES); + tryStartingKbMode(-1); + mHourView.invalidate(); + } else if (mTypedTimes == null) { + mTypedTimes = new ArrayList<>(); + } + + // Set the title (if any) + TextView timePickerHeader = view.findViewById(R.id.time_picker_header); + if (!mTitle.isEmpty()) { + timePickerHeader.setVisibility(TextView.VISIBLE); + timePickerHeader.setText(mTitle); // TODO + } + + // Set the theme at the end so that the initialize()s above don't counteract the theme. + mTimePicker.setTheme(getActivity().getApplicationContext(), mThemeDark); + // Prepare some colors to use. + int white = res.getColor(R.color.mdtp_white); + int accent = res.getColor(R.color.mdtp_white); + int circleBackground = res.getColor(R.color.mdtp_circle_background); + int line = res.getColor(R.color.mdtp_white); + int timeDisplay = res.getColor(R.color.mdtp_numbers_text_color); + ColorStateList doneTextColor = res.getColorStateList(R.color.mdtp_done_text_color); + int doneBackground = R.drawable.mdtp_done_background_color; + int backgroundColor = res.getColor(R.color.mdtp_background_color); + int darkBackgroundColor = res.getColor(R.color.mdtp_light_gray); + + int darkGray = res.getColor(R.color.mdtp_dark_gray); + int lightGray = res.getColor(R.color.mdtp_light_gray); + int darkLine = res.getColor(R.color.mdtp_line_dark); + ColorStateList darkDoneTextColor = res.getColorStateList(R.color.mdtp_done_text_color_dark); + int darkDoneBackground = R.drawable.mdtp_done_background_color_dark; + + // Set the colors for each view based on the theme. + //view.findViewById(R.id.time_display_background).setBackgroundColor(mThemeDark? darkGray : accent); + //view.findViewById(R.id.time_display).setBackgroundColor(mThemeDark? darkGray : white); + //((TextView) view.findViewById(R.id.separator)).setTextColor(mThemeDark? white : timeDisplay); + //((TextView) view.findViewById(R.id.ampm_label)).setTextColor(mThemeDark? white : timeDisplay); + //view.findViewById(R.id.line).setBackgroundColor(mThemeDark? darkLine : line); + //mOkButton.setTextColor(mThemeDark? darkDoneTextColor : doneTextColor); + mTimePicker.setBackgroundColor(mThemeDark? lightGray : circleBackground); + view.findViewById(R.id.time_picker_dialog).setBackgroundColor(mThemeDark ? darkBackgroundColor : backgroundColor); + //mOkButton.setBackgroundResource(mThemeDark? darkDoneBackground : doneBackground); + return view; + } + + @Override + public void onResume() { + super.onResume(); + mHapticFeedbackController.start(); + } + + @Override + public void onPause() { + super.onPause(); + mHapticFeedbackController.stop(); + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + if(mOnCancelListener != null) mOnCancelListener.onCancel(dialog); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if(mOnDismissListener != null) mOnDismissListener.onDismiss(dialog); + } + + private void tryVibrate() { + mHapticFeedbackController.tryVibrate(); + } + + private void updateAmPmDisplay(int amOrPm) { + if (amOrPm == AM) { + mAmPmTextView.setText(mAmText); + Utils.tryAccessibilityAnnounce(mTimePicker, mAmText); + mAmPmHitspace.setContentDescription(mAmText); + } else if (amOrPm == PM){ + mAmPmTextView.setText(mPmText); + Utils.tryAccessibilityAnnounce(mTimePicker, mPmText); + mAmPmHitspace.setContentDescription(mPmText); + } else { + mAmPmTextView.setText(mDoublePlaceholderText); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + if (mTimePicker != null) { + outState.putInt(KEY_HOUR_OF_DAY, mTimePicker.getHours()); + outState.putInt(KEY_MINUTE, mTimePicker.getMinutes()); + outState.putBoolean(KEY_IS_24_HOUR_VIEW, mIs24HourMode); + outState.putInt(KEY_CURRENT_ITEM_SHOWING, mTimePicker.getCurrentItemShowing()); + outState.putBoolean(KEY_IN_KB_MODE, mInKbMode); + if (mInKbMode) { + outState.putIntegerArrayList(KEY_TYPED_TIMES, mTypedTimes); + } + outState.putString(KEY_TITLE, mTitle); + outState.putBoolean(KEY_DARK_THEME, mThemeDark); + } + } + + /** + * Called by the picker for updating the header display. + */ + @Override + public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) { + if (pickerIndex == HOUR_INDEX) { + setHour(newValue, false); + String announcement = String.format("%d", newValue); + if (mAllowAutoAdvance && autoAdvance) { + setCurrentItemShowing(MINUTE_INDEX, true, true, false); + announcement += ". " + mSelectMinutes; + } else { + mTimePicker.setContentDescription(mHourPickerDescription + ": " + newValue); + } + + Utils.tryAccessibilityAnnounce(mTimePicker, announcement); + } else if (pickerIndex == MINUTE_INDEX){ + setMinute(newValue); + mTimePicker.setContentDescription(mMinutePickerDescription + ": " + newValue); + } else if (pickerIndex == AMPM_INDEX) { + updateAmPmDisplay(newValue); + } else if (pickerIndex == ENABLE_PICKER_INDEX) { + if (!isTypedTimeFullyLegal()) { + mTypedTimes.clear(); + } + finishKbMode(true); + } + } + + private void setHour(int value, boolean announce) { + String format; + if (mIs24HourMode) { + format = "%02d"; + } else { + format = "%d"; + value = value % 12; + if (value == 0) { + value = 12; + } + } + + String text = LanguageUtils.getPersianNumbers(String.format(format, value)); + mHourView.setText(text); + mHourSpaceView.setText(text); + if (announce) { + Utils.tryAccessibilityAnnounce(mTimePicker, text); + } + } + + private void setMinute(int value) { + if (value == 60) { + value = 0; + } + CharSequence text = LanguageUtils.getPersianNumbers(String.format(Locale.getDefault(), "%02d", value)); + Utils.tryAccessibilityAnnounce(mTimePicker, text); + mMinuteView.setText(text); + mMinuteSpaceView.setText(text); + } + + // Show either Hours or Minutes. + private void setCurrentItemShowing(int index, boolean animateCircle, boolean delayLabelAnimate, + boolean announce) { + mTimePicker.setCurrentItemShowing(index, animateCircle); + + TextView labelToAnimate; + if (index == HOUR_INDEX) { + int hours = mTimePicker.getHours(); + if (!mIs24HourMode) { + hours = hours % 12; + } + mTimePicker.setContentDescription(mHourPickerDescription + ": " + hours); + if (announce) { + Utils.tryAccessibilityAnnounce(mTimePicker, mSelectHours); + } + labelToAnimate = mHourView; + } else { + int minutes = mTimePicker.getMinutes(); + mTimePicker.setContentDescription(mMinutePickerDescription + ": " + minutes); + if (announce) { + Utils.tryAccessibilityAnnounce(mTimePicker, mSelectMinutes); + } + labelToAnimate = mMinuteView; + } + + int hourColor = (index == HOUR_INDEX)? mSelectedColor : mUnselectedColor; + int minuteColor = (index == MINUTE_INDEX)? mSelectedColor : mUnselectedColor; + mHourView.setTextColor(hourColor); + mMinuteView.setTextColor(minuteColor); + + ObjectAnimator pulseAnimator = Utils.getPulseAnimator(labelToAnimate, 0.85f, 1.1f); + if (delayLabelAnimate) { + pulseAnimator.setStartDelay(PULSE_ANIMATOR_DELAY); + } + pulseAnimator.start(); + } + + /** + * For keyboard mode, processes key events. + * @param keyCode the pressed key. + * @return true if the key was successfully processed, false otherwise. + */ + private boolean processKeyUp(int keyCode) { + if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) { + if(isCancelable()) dismiss(); + return true; + } else if (keyCode == KeyEvent.KEYCODE_TAB) { + if(mInKbMode) { + if (isTypedTimeFullyLegal()) { + finishKbMode(true); + } + return true; + } + } else if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (mInKbMode) { + if (!isTypedTimeFullyLegal()) { + return true; + } + finishKbMode(false); + } + if (mCallback != null) { + mCallback.onTimeSet(mTimePicker, + mTimePicker.getHours(), mTimePicker.getMinutes()); + } + dismiss(); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DEL) { + if (mInKbMode) { + if (!mTypedTimes.isEmpty()) { + int deleted = deleteLastTypedKey(); + String deletedKeyStr; + if (deleted == getAmOrPmKeyCode(AM)) { + deletedKeyStr = mAmText; + } else if (deleted == getAmOrPmKeyCode(PM)) { + deletedKeyStr = mPmText; + } else { + deletedKeyStr = String.format("%d", getValFromKeyCode(deleted)); //TODO + } + Utils.tryAccessibilityAnnounce(mTimePicker, + String.format(mDeletedKeyFormat, deletedKeyStr)); + updateDisplay(true); + } + } + } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1 + || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3 + || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5 + || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7 + || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9 + || (!mIs24HourMode && + (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) { + if (!mInKbMode) { + if (mTimePicker == null) { + // Something's wrong, because time picker should definitely not be null. + Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null."); + return true; + } + mTypedTimes.clear(); + tryStartingKbMode(keyCode); + return true; + } + // We're already in keyboard mode. + if (addKeyIfLegal(keyCode)) { + updateDisplay(false); + } + return true; + } + return false; + } + + /** + * Try to start keyboard mode with the specified key, as long as the timepicker is not in the + * middle of a touch-event. + * @param keyCode The key to use as the first press. Keyboard mode will not be started if the + * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting + * key. + */ + private void tryStartingKbMode(int keyCode) { + if (mTimePicker.trySettingInputEnabled(false) && + (keyCode == -1 || addKeyIfLegal(keyCode))) { + mInKbMode = true; + mOkButton.setEnabled(false); + updateDisplay(false); + } + } + + private boolean addKeyIfLegal(int keyCode) { + // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode, + // we'll need to see if AM/PM have been typed. + if ((mIs24HourMode && mTypedTimes.size() == 4) || + (!mIs24HourMode && isTypedTimeFullyLegal())) { + return false; + } + + mTypedTimes.add(keyCode); //TODO + if (!isTypedTimeLegalSoFar()) { + deleteLastTypedKey(); + return false; + } + + int val = getValFromKeyCode(keyCode); + Utils.tryAccessibilityAnnounce(mTimePicker, String.format("%d", val)); + // Automatically fill in 0's if AM or PM was legally entered. + if (isTypedTimeFullyLegal()) { + if (!mIs24HourMode && mTypedTimes.size() <= 3) { + mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); + mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); + } + mOkButton.setEnabled(true); + } + + return true; + } + + /** + * Traverse the tree to see if the keys that have been typed so far are legal as is, + * or may become legal as more keys are typed (excluding backspace). + */ + private boolean isTypedTimeLegalSoFar() { + Node node = mLegalTimesTree; + for (int keyCode : mTypedTimes) { + node = node.canReach(keyCode); + if (node == null) { + return false; + } + } + return true; + } + + /** + * Check if the time that has been typed so far is completely legal, as is. + */ + private boolean isTypedTimeFullyLegal() { + if (mIs24HourMode) { + // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note: + // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode. + int[] values = getEnteredTime(null); + return (values[0] >= 0 && values[1] >= 0 && values[1] < 60); + } else { + // For AM/PM mode, the time is legal if it contains an AM or PM, as those can only be + // legally added at specific times based on the tree's algorithm. + return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) || + mTypedTimes.contains(getAmOrPmKeyCode(PM))); + } + } + + private int deleteLastTypedKey() { + int deleted = mTypedTimes.remove(mTypedTimes.size() - 1); + if (!isTypedTimeFullyLegal()) { + mOkButton.setEnabled(false); + } + return deleted; + } + + /** + * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time. + * @param updateDisplays If true, update the displays with the relevant time. + */ + private void finishKbMode(boolean updateDisplays) { + mInKbMode = false; + if (!mTypedTimes.isEmpty()) { + int values[] = getEnteredTime(null); + mTimePicker.setTime(values[0], values[1]); + if (!mIs24HourMode) { + mTimePicker.setAmOrPm(values[2]); + } + mTypedTimes.clear(); + } + if (updateDisplays) { + updateDisplay(false); + mTimePicker.trySettingInputEnabled(true); + } + } + + /** + * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is + * empty, either show an empty display (filled with the placeholder text), or update from the + * timepicker's values. + * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text. + * Otherwise, revert to the timepicker's values. + */ + private void updateDisplay(boolean allowEmptyDisplay) { + if (!allowEmptyDisplay && mTypedTimes.isEmpty()) { + int hour = mTimePicker.getHours(); + int minute = mTimePicker.getMinutes(); + setHour(hour, true); + setMinute(minute); + if (!mIs24HourMode) { + updateAmPmDisplay(hour < 12? AM : PM); + } + setCurrentItemShowing(mTimePicker.getCurrentItemShowing(), true, true, true); + mOkButton.setEnabled(true); + } else { + Boolean[] enteredZeros = {false, false}; + int[] values = getEnteredTime(enteredZeros); + String hourFormat = enteredZeros[0]? "%02d" : "%2d"; + String minuteFormat = (enteredZeros[1])? "%02d" : "%2d"; + String hourStr = (values[0] == -1)? mDoublePlaceholderText : + String.format(hourFormat, values[0]).replace(' ', mPlaceholderText); + String minuteStr = (values[1] == -1)? mDoublePlaceholderText : + String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText); + mHourView.setText(LanguageUtils.getPersianNumbers(hourStr)); + mHourSpaceView.setText(LanguageUtils.getPersianNumbers(hourStr)); + mHourView.setTextColor(mUnselectedColor); + mMinuteView.setText(LanguageUtils.getPersianNumbers(minuteStr)); + mMinuteSpaceView.setText(LanguageUtils.getPersianNumbers(minuteStr)); + mMinuteView.setTextColor(mUnselectedColor); + if (!mIs24HourMode) { + updateAmPmDisplay(values[2]); + } + } + } + + private static int getValFromKeyCode(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_0: + return 0; + case KeyEvent.KEYCODE_1: + return 1; + case KeyEvent.KEYCODE_2: + return 2; + case KeyEvent.KEYCODE_3: + return 3; + case KeyEvent.KEYCODE_4: + return 4; + case KeyEvent.KEYCODE_5: + return 5; + case KeyEvent.KEYCODE_6: + return 6; + case KeyEvent.KEYCODE_7: + return 7; + case KeyEvent.KEYCODE_8: + return 8; + case KeyEvent.KEYCODE_9: + return 9; + default: + return -1; + } + } + + /** + * Get the currently-entered time, as integer values of the hours and minutes typed. + * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which + * may then be used for the caller to know whether zeros had been explicitly entered as either + * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's. + * @return A size-3 int array. The first value will be the hours, the second value will be the + * minutes, and the third will be either TimePickerDialog.AM or TimePickerDialog.PM. + */ + private int[] getEnteredTime(Boolean[] enteredZeros) { + int amOrPm = -1; + int startIndex = 1; + if (!mIs24HourMode && isTypedTimeFullyLegal()) { + int keyCode = mTypedTimes.get(mTypedTimes.size() - 1); + if (keyCode == getAmOrPmKeyCode(AM)) { + amOrPm = AM; + } else if (keyCode == getAmOrPmKeyCode(PM)){ + amOrPm = PM; + } + startIndex = 2; + } + int minute = -1; + int hour = -1; + for (int i = startIndex; i <= mTypedTimes.size(); i++) { + int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i)); + if (i == startIndex) { + minute = val; + } else if (i == startIndex+1) { + minute += 10*val; + if (enteredZeros != null && val == 0) { + enteredZeros[1] = true; + } + } else if (i == startIndex+2) { + hour = val; + } else if (i == startIndex+3) { + hour += 10*val; + if (enteredZeros != null && val == 0) { + enteredZeros[0] = true; + } + } + } + + return new int[] {hour, minute, amOrPm}; + } + + /** + * Get the keycode value for AM and PM in the current language. + */ + private int getAmOrPmKeyCode(int amOrPm) { + // Cache the codes. + if (mAmKeyCode == -1 || mPmKeyCode == -1) { + // Find the first character in the AM/PM text that is unique. + KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + char amChar; + char pmChar; + for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) { + amChar = "AM".toLowerCase(Locale.getDefault()).charAt(i); + pmChar = "PM".toLowerCase(Locale.getDefault()).charAt(i); + if (amChar != pmChar) { + KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar}); + // There should be 4 events: a down and up for both AM and PM. + if (events != null && events.length == 4) { + mAmKeyCode = events[0].getKeyCode(); + mPmKeyCode = events[2].getKeyCode(); + } else { + Log.e(TAG, "Unable to find keycodes for AM and PM."); + } + break; + } + } + } + if (amOrPm == AM) { + return mAmKeyCode; + } else if (amOrPm == PM) { + return mPmKeyCode; + } + + return -1; + } + + /** + * Create a tree for deciding what keys can legally be typed. + */ + private void generateLegalTimesTree() { + // Create a quick cache of numbers to their keycodes. + int k0 = KeyEvent.KEYCODE_0; + int k1 = KeyEvent.KEYCODE_1; + int k2 = KeyEvent.KEYCODE_2; + int k3 = KeyEvent.KEYCODE_3; + int k4 = KeyEvent.KEYCODE_4; + int k5 = KeyEvent.KEYCODE_5; + int k6 = KeyEvent.KEYCODE_6; + int k7 = KeyEvent.KEYCODE_7; + int k8 = KeyEvent.KEYCODE_8; + int k9 = KeyEvent.KEYCODE_9; + + // The root of the tree doesn't contain any numbers. + mLegalTimesTree = new Node(); + if (mIs24HourMode) { + // We'll be re-using these nodes, so we'll save them. + Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5); + Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + // The first digit must be followed by the second digit. + minuteFirstDigit.addChild(minuteSecondDigit); + + // The first digit may be 0-1. + Node firstDigit = new Node(k0, k1); + mLegalTimesTree.addChild(firstDigit); + + // When the first digit is 0-1, the second digit may be 0-5. + Node secondDigit = new Node(k0, k1, k2, k3, k4, k5); + firstDigit.addChild(secondDigit); + // We may now be followed by the first minute digit. E.g. 00:09, 15:58. + secondDigit.addChild(minuteFirstDigit); + + // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9. + Node thirdDigit = new Node(k6, k7, k8, k9); + // The time must now be finished. E.g. 0:55, 1:08. + secondDigit.addChild(thirdDigit); + + // When the first digit is 0-1, the second digit may be 6-9. + secondDigit = new Node(k6, k7, k8, k9); + firstDigit.addChild(secondDigit); + // We must now be followed by the first minute digit. E.g. 06:50, 18:20. + secondDigit.addChild(minuteFirstDigit); + + // The first digit may be 2. + firstDigit = new Node(k2); + mLegalTimesTree.addChild(firstDigit); + + // When the first digit is 2, the second digit may be 0-3. + secondDigit = new Node(k0, k1, k2, k3); + firstDigit.addChild(secondDigit); + // We must now be followed by the first minute digit. E.g. 20:50, 23:09. + secondDigit.addChild(minuteFirstDigit); + + // When the first digit is 2, the second digit may be 4-5. + secondDigit = new Node(k4, k5); + firstDigit.addChild(secondDigit); + // We must now be followd by the last minute digit. E.g. 2:40, 2:53. + secondDigit.addChild(minuteSecondDigit); + + // The first digit may be 3-9. + firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9); + mLegalTimesTree.addChild(firstDigit); + // We must now be followed by the first minute digit. E.g. 3:57, 8:12. + firstDigit.addChild(minuteFirstDigit); + } else { + // We'll need to use the AM/PM node a lot. + // Set up AM and PM to respond to "a" and "p". + Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM)); + + // The first hour digit may be 1. + Node firstDigit = new Node(k1); + mLegalTimesTree.addChild(firstDigit); + // We'll allow quick input of on-the-hour times. E.g. 1pm. + firstDigit.addChild(ampm); + + // When the first digit is 1, the second digit may be 0-2. + Node secondDigit = new Node(k0, k1, k2); + firstDigit.addChild(secondDigit); + // Also for quick input of on-the-hour times. E.g. 10pm, 12am. + secondDigit.addChild(ampm); + + // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5. + Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5); + secondDigit.addChild(thirdDigit); + // The time may be finished now. E.g. 1:02pm, 1:25am. + thirdDigit.addChild(ampm); + + // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5, + // the fourth digit may be 0-9. + Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + thirdDigit.addChild(fourthDigit); + // The time must be finished now. E.g. 10:49am, 12:40pm. + fourthDigit.addChild(ampm); + + // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9. + thirdDigit = new Node(k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 1:08am, 1:26pm. + thirdDigit.addChild(ampm); + + // When the first digit is 1, the second digit may be 3-5. + secondDigit = new Node(k3, k4, k5); + firstDigit.addChild(secondDigit); + + // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9. + thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 1:39am, 1:50pm. + thirdDigit.addChild(ampm); + + // The hour digit may be 2-9. + firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9); + mLegalTimesTree.addChild(firstDigit); + // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm. + firstDigit.addChild(ampm); + + // When the first digit is 2-9, the second digit may be 0-5. + secondDigit = new Node(k0, k1, k2, k3, k4, k5); + firstDigit.addChild(secondDigit); + + // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9. + thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 2:57am, 9:30pm. + thirdDigit.addChild(ampm); + } + } + + /** + * Simple node class to be used for traversal to check for legal times. + * mLegalKeys represents the keys that can be typed to get to the node. + * mChildren are the children that can be reached from this node. + */ + private static class Node { + private int[] mLegalKeys; + private ArrayList mChildren; + + public Node(int... legalKeys) { + mLegalKeys = legalKeys; + mChildren = new ArrayList<>(); + } + + public void addChild(Node child) { + mChildren.add(child); + } + + public boolean containsKey(int key) { + for (int mLegalKey : mLegalKeys) { + if (mLegalKey == key) { + return true; + } + } + return false; + } + + public Node canReach(int key) { + if (mChildren == null) { + return null; + } + for (Node child : mChildren) { + if (child.containsKey(key)) { + return child; + } + } + return null; + } + } + + private class KeyboardListener implements OnKeyListener { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + return event.getAction() == KeyEvent.ACTION_UP && processKeyUp(keyCode); + } + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/LanguageUtils.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/LanguageUtils.java new file mode 100644 index 0000000..1acd7a3 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/LanguageUtils.java @@ -0,0 +1,48 @@ +package mohammadaminha.com.widgets.Date_Picker.utils; + +import java.util.ArrayList; + +public class LanguageUtils { + + public static String getPersianNumbers(String string) { + string = string.replace("0", "۰"); + string = string.replace("1", "١"); + string = string.replace("2", "۲"); + string = string.replace("3", "۳"); + string = string.replace("4", "۴"); + string = string.replace("5", "۵"); + string = string.replace("6", "۶"); + string = string.replace("7", "۷"); + string = string.replace("8", "۸"); + string = string.replace("9", "۹"); + return string; + } + + public static void getPersianNumbers(String[] strings) { + for (int i=0; i getPersianNumbers(ArrayList strings) { + for (int i=0; i Persian(Shamsi) calendar + *

    + *

    + *

    + * The calendar consists of 12 months, the first six of which are 31 days, the + * next five 30 days, and the final month 29 days in a normal year and 30 days + * in a leap year. + *

    + *

    + * As one of the few calendars designed in the era of accurate positional + * astronomy, the Persian calendar uses a very complex leap year structure which + * makes it the most accurate solar calendar in use today. Years are grouped + * into cycles which begin with four normal years after which every fourth + * subsequent year in the cycle is a leap year. Cycles are grouped into grand + * cycles of either 128 years (composed of cycles of 29, 33, 33, and 33 years) + * or 132 years, containing cycles of of 29, 33, 33, and 37 years. A great grand + * cycle is composed of 21 consecutive 128 year grand cycles and a final 132 + * grand cycle, for a total of 2820 years. The pattern of normal and leap years + * which began in 1925 will not repeat until the year 4745! + *

    + *

    Each 2820 year great grand cycle contains 2137 normal years of 365 days + * and 683 leap years of 366 days, with the average year length over the great + * grand cycle of 365.24219852. So close is this to the actual solar tropical + * year of 365.24219878 days that the Persian calendar accumulates an error of + * one day only every 3.8 million years. As a purely solar calendar, months are + * not synchronized with the phases of the Moon.

    + *

    + *

    + *

    + *

    + * PersianCalendar by extending Default GregorianCalendar + * provides capabilities such as: + *

    + *

    + *

    + *

    + *

  • you can set the date in Persian by setPersianDate(persianYear, + * persianMonth, persianDay) and get the Gregorian date or vice versa
  • + *

    + *

    + *
  • determine is the current date is Leap year in persian calendar or not by + * IsPersianLeapYear()
  • + *

    + *

    + *
  • getPersian short and long Date String getPersianShortDate() and + * getPersianLongDate you also can set delimiter to assign delimiter of returned + * dateString
  • + *

    + *

    + *
  • Parse string based on assigned delimiter
  • + *

    + *

    + *

    + *

    + *

    + *

    + *

    + * Example + *

    + *

    + *

    + *

    + *

    + * {@code
    + *       PersianCalendar persianCal = new PersianCalendar();
    + *       System.out.println(persianCal.getPersianShortDate());
    + *
    + *       persianCal.set(1982, Calendar.MAY, 22);
    + *       System.out.println(persianCal.getPersianShortDate());
    + *
    + *       persianCal.setDelimiter(" , ");
    + *       persianCal.parse("1361 , 03 , 01");
    + *       System.out.println(persianCal.getPersianShortDate());
    + *
    + *       persianCal.setPersianDate(1361, 3, 1);
    + *       System.out.println(persianCal.getPersianLongDate());
    + *       System.out.println(persianCal.getTime());
    + *
    + *       persianCal.addPersianDate(Calendar.MONTH, 33);
    + *       persianCal.addPersianDate(Calendar.YEAR, 5);
    + *       persianCal.addPersianDate(Calendar.DATE, 50);
    + *
    + * }
    + *
    + * 
    + * @author Morteza  contact: Mortezaadi@gmail.com
    + * @version 1.1
    + */
    +public class PersianCalendar extends GregorianCalendar {
    +
    +	private static final long serialVersionUID = 5541422440580682494L;
    +
    +	private int persianYear;
    +	private int persianMonth;
    +	private int persianDay;
    +	// use to seperate PersianDate's field and also Parse the DateString based
    +	// on this delimiter
    +	private String delimiter = "/";
    +
    +	private long convertToMilis(long julianDate) {
    +		return PersianCalendarConstants.MILLIS_JULIAN_EPOCH + julianDate * PersianCalendarConstants.MILLIS_OF_A_DAY
    +				+ PersianCalendarUtils.ceil(getTimeInMillis() - PersianCalendarConstants.MILLIS_JULIAN_EPOCH, PersianCalendarConstants.MILLIS_OF_A_DAY);
    +	}
    +
    +	/**
    +	 * default constructor
    +	 * 

    + * most of the time we don't care about TimeZone when we persisting Date or + * doing some calculation on date. Default TimeZone was set to + * "GMT" in order to make developer to work more convenient with + * the library; however you can change the TimeZone as you do in + * GregorianCalendar by calling setTimeZone() + */ + public PersianCalendar(long millis) { + setTimeInMillis(millis); + } + + /** + * default constructor + *

    + * most of the time we don't care about TimeZone when we persisting Date or + * doing some calculation on date. Default TimeZone was set to + * "GMT" in order to make developer to work more convenient with + * the library; however you can change the TimeZone as you do in + * GregorianCalendar by calling setTimeZone() + */ + public PersianCalendar() { + setTimeZone(TimeZone.getTimeZone("Iran")); + } + + /** + * Calculate persian date from current Date and populates the corresponding + * fields(persianYear, persianMonth, persianDay) + */ + private void calculatePersianDate() { + long julianDate = ((long) Math.floor((getTimeInMillis() - PersianCalendarConstants.MILLIS_JULIAN_EPOCH)) / PersianCalendarConstants.MILLIS_OF_A_DAY); + long PersianRowDate = PersianCalendarUtils.julianToPersian(julianDate); + long year = PersianRowDate >> 16; + int month = (int) (PersianRowDate & 0xff00) >> 8; + int day = (int) (PersianRowDate & 0xff); + this.persianYear = (int) (year > 0 ? year : year - 1); + this.persianMonth = month; + this.persianDay = day; + } + + /** + * Determines if the given year is a leap year in persian calendar. Returns + * true if the given year is a leap year. + * + * @return boolean + */ + public boolean isPersianLeapYear() { + // calculatePersianDate(); + return PersianCalendarUtils.isPersianLeapYear(this.persianYear); + } + + /** + * set the persian date it converts PersianDate to the Julian and assigned + * equivalent milliseconds to the instance + * + * @param persianYear + * @param persianMonth + * @param persianDay + */ + public void setPersianDate(int persianYear, int persianMonth, int persianDay) { + persianMonth += 1; // TODO + this.persianYear = persianYear; + this.persianMonth = persianMonth; + this.persianDay = persianDay; + setTimeInMillis(convertToMilis(PersianCalendarUtils.persianToJulian(this.persianYear > 0 ? this.persianYear : this.persianYear + 1, this.persianMonth - 1, this.persianDay))); + } + + public int getPersianYear() { + // calculatePersianDate(); + return this.persianYear; + } + + /** + * @return int persian month number + */ + public int getPersianMonth() { + // calculatePersianDate(); + return this.persianMonth; + } + + /** + * @return String persian month name + */ + public String getPersianMonthName() { + // calculatePersianDate(); + return PersianCalendarConstants.persianMonthNames[this.persianMonth]; + } + + /** + * @return int Persian day in month + */ + public int getPersianDay() { + // calculatePersianDate(); + return this.persianDay; + } + + /** + * @return String Name of the day in week + */ + public String getPersianWeekDayName() { + switch (get(DAY_OF_WEEK)) { + case SATURDAY: + return PersianCalendarConstants.persianWeekDays[0]; + case SUNDAY: + return PersianCalendarConstants.persianWeekDays[1]; + case MONDAY: + return PersianCalendarConstants.persianWeekDays[2]; + case TUESDAY: + return PersianCalendarConstants.persianWeekDays[3]; + case WEDNESDAY: + return PersianCalendarConstants.persianWeekDays[4]; + case THURSDAY: + return PersianCalendarConstants.persianWeekDays[5]; + default: + return PersianCalendarConstants.persianWeekDays[6]; + } + + } + + /** + * @return String of Persian Date ex: شنبه 01 خرداد 1361 + */ + public String getPersianLongDate() { + return getPersianWeekDayName() + " " + this.persianDay + " " + getPersianMonthName() + " " + this.persianYear; + } + + public String getPersianLongDateAndTime() { + return getPersianLongDate() + " ساعت " + get(HOUR_OF_DAY) + ":" + get(MINUTE) + ":" + get(SECOND); + } + + /** + * @return String of persian date formatted by + * 'YYYY[delimiter]mm[delimiter]dd' default delimiter is '/' + */ + private String getPersianShortDate() { + // calculatePersianDate(); + return "" + formatToMilitary(this.persianYear) + delimiter + formatToMilitary(getPersianMonth() + 1) + delimiter + formatToMilitary(this.persianDay); + } + + public String getPersianShortDateTime() { + return + //formatToMilitary(this.persianYear) + delimiter + + //formatToMilitary(getPersianMonth() + 1) + delimiter + + //formatToMilitary(this.persianDay) + " " + + (this.persianYear) + delimiter + + (getPersianMonth() + 1) + delimiter + + (this.persianDay) + " " + + formatToMilitary(this.get(HOUR_OF_DAY)) + + ":" + + formatToMilitary(get(MINUTE)); + } + + public String getPersianHour() { + return + formatToMilitary(get(HOUR_OF_DAY)) + + ":" + + formatToMilitary(get(MINUTE)); + } + + public String formatToMilitary(int i) { + + return (i < 10) ? "0" + i : String.valueOf(i); + } + + /** + * add specific amout of fields to the current date for now doesnt handle + * before 1 farvardin hejri (before epoch) + * + * @param field + * @param amount

    +	 *                                                          Usage:
    +	 *                                                          {@code
    +	 *                                                          addPersianDate(Calendar.YEAR, 2);
    +	 *                                                          addPersianDate(Calendar.MONTH, 3);
    +	 *                                                          }
    +	 *                                                         
    + *

    + * u can also use Calendar.HOUR_OF_DAY,Calendar.MINUTE, + * Calendar.SECOND, Calendar.MILLISECOND etc + */ + // + public void addPersianDate(int field, int amount) { + if (amount == 0) { + return; // Do nothing! + } + + if (field < 0 || field >= ZONE_OFFSET) { + throw new IllegalArgumentException(); + } + + if (field == YEAR) { + setPersianDate(this.persianYear + amount, getPersianMonth() + 1, this.persianDay); + return; + } else if (field == MONTH) { + setPersianDate(this.persianYear + ((getPersianMonth() + 1 + amount) / 12), (getPersianMonth() + 1 + amount) % 12, this.persianDay); + return; + } + add(field, amount); + calculatePersianDate(); + } + + /** + *

    +	 *    use {@link PersianDateParser} to parse string
    +	 *    and get the Persian Date.
    +	 * 
    + * + * @param dateString + * @see PersianDateParser + */ + public void parse(String dateString) { + PersianCalendar p = new PersianDateParser(dateString, delimiter).getPersianDate(); + setPersianDate(p.getPersianYear(), p.getPersianMonth(), p.getPersianDay()); + } + + public String getDelimiter() { + return delimiter; + } + + /** + * assign delimiter to use as a separator of date fields. + * + * @param delimiter + */ + public void setDelimiter(String delimiter) { + this.delimiter = delimiter; + } + + @Override + public String toString() { + String str = super.toString(); + return str.substring(0, str.length() - 1) + ",PersianDate=" + getPersianShortDate() + "]"; + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public void set(int field, int value) { + super.set(field, value); + calculatePersianDate(); + } + + @Override + public void setTimeInMillis(long millis) { + super.setTimeInMillis(millis); + calculatePersianDate(); + } + + @Override + public void setTimeZone(TimeZone zone) { + super.setTimeZone(zone); + calculatePersianDate(); + } + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianCalendarConstants.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianCalendarConstants.java new file mode 100644 index 0000000..a13208c --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianCalendarConstants.java @@ -0,0 +1,46 @@ +package mohammadaminha.com.widgets.Date_Picker.utils; + +/** + * + * @author Morteza contact: Mortezaadi@gmail.com + * @version 1.0 + */ +class PersianCalendarConstants { + + // 00:00:00 UTC (Gregorian) Julian day 0, + // 0 milliseconds since 1970-01-01 + public static final long MILLIS_JULIAN_EPOCH = -210866803200000L; + // Milliseconds of a day calculated by 24L(hours) * 60L(minutes) * + // 60L(seconds) * 1000L(mili); + public static final long MILLIS_OF_A_DAY = 86400000L; + + /** + * The JDN of 1 Farvardin 1; Equivalent to March 19, 622 A.D. + */ + public static final long PERSIAN_EPOCH = 1948321; + + public static final String[] persianMonthNames = { "\u0641\u0631\u0648\u0631\u062f\u06cc\u0646", // Farvardin + "\u0627\u0631\u062f\u06cc\u0628\u0647\u0634\u062a", // Ordibehesht + "\u062e\u0631\u062f\u0627\u062f", // Khordad + "\u062a\u06cc\u0631", // Tir + "\u0645\u0631\u062f\u0627\u062f", // Mordad + "\u0634\u0647\u0631\u06cc\u0648\u0631", // Shahrivar + "\u0645\u0647\u0631", // Mehr + "\u0622\u0628\u0627\u0646", // Aban + "\u0622\u0630\u0631", // Azar + "\u062f\u06cc", // Dey + "\u0628\u0647\u0645\u0646", // Bahman + "\u0627\u0633\u0641\u0646\u062f" // Esfand + }; + + public static final String[] persianWeekDays = { "\u0634\u0646\u0628\u0647", // Shanbeh + "\u06cc\u06a9\u200c\u0634\u0646\u0628\u0647", // Yekshanbeh + "\u062f\u0648\u0634\u0646\u0628\u0647", // Doshanbeh + "\u0633\u0647\u200c\u0634\u0646\u0628\u0647", // Sehshanbeh + "\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647", // Chaharshanbeh + "\u067e\u0646\u062c\u200c\u0634\u0646\u0628\u0647", // Panjshanbeh + "\u062c\u0645\u0639\u0647" // jome + }; + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianCalendarUtils.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianCalendarUtils.java new file mode 100644 index 0000000..b54a56e --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianCalendarUtils.java @@ -0,0 +1,80 @@ +package mohammadaminha.com.widgets.Date_Picker.utils; + +/** + * algorithms for converting Julian days to the Persian calendar, and vice versa + * are adopted from couprie.nl written in + * VB. The algorithms is not exactly the same as its original. I've done some + * minor changes in the sake of performances and corrected some bugs. + * + * @author Morteza contact: Mortezaadi@gmail.com + * @version 1.0 + * + */ +public class PersianCalendarUtils { + + /** + * Converts a provided Persian (Shamsi) date to the Julian Day Number (i.e. + * the number of days since January 1 in the year 4713 BC). Since the + * Persian calendar is a highly regular calendar, converting to and from a + * Julian Day Number is not as difficult as it looks. Basically it's a + * mather of dividing, rounding and multiplying. This routine uses Julian + * Day Number 1948321 as focal point, since that Julian Day Number + * corresponds with 1 Farvardin (1) 1. + * + * @param year + * int persian year + * @param month + * int persian month + * @param day + * int persian day + * @return long + */ + public static long persianToJulian(long year, int month, int day) { + return 365L * ((ceil(year - 474L, 2820D) + 474L) - 1L) + ((long) Math.floor((682L * (ceil(year - 474L, 2820D) + 474L) - 110L) / 2816D)) + (PersianCalendarConstants.PERSIAN_EPOCH - 1L) + 1029983L + * ((long) Math.floor((year - 474L) / 2820D)) + (month < 7 ? 31 * month : 30 * month + 6) + day; + } + + /** + * Calculate whether current year is Leap year in persian or not + * + * @return boolean + */ + public static boolean isPersianLeapYear(int persianYear) { + return PersianCalendarUtils.ceil((38D + (PersianCalendarUtils.ceil(persianYear - 474L, 2820L) + 474L)) * 682D, 2816D) < 682L; + } + + /** + * Converts a provided Julian Day Number (i.e. the number of days since + * January 1 in the year 4713 BC) to the Persian (Shamsi) date. Since the + * Persian calendar is a highly regular calendar, converting to and from a + * Julian Day Number is not as difficult as it looks. Basically it's a + * mather of dividing, rounding and multiplying. + * + * @param julianDate + * @return long + */ + public static long julianToPersian(long julianDate) { + long persianEpochInJulian = julianDate - persianToJulian(475L, 0, 1); + long cyear = ceil(persianEpochInJulian, 1029983D); + long ycycle = cyear != 1029982L ? ((long) Math.floor((2816D * (double) cyear + 1031337D) / 1028522D)) : 2820L; + long year = 474L + 2820L * ((long) Math.floor(persianEpochInJulian / 1029983D)) + ycycle; + long aux = (1L + julianDate) - persianToJulian(year, 0, 1); + int month = (int) (aux > 186L ? Math.ceil((double) (aux - 6L) / 30D) - 1 : Math.ceil((double) aux / 31D) - 1); + int day = (int) (julianDate - (persianToJulian(year, month, 1) - 1L)); + return (year << 16) | (month << 8) | day; + } + + /** + * Ceil function in original algorithm + * + * @param double1 + * @param double2 + * @return long + */ + public static long ceil(double double1, double double2) { + return (long) (double1 - double2 * Math.floor(double1 / double2)); + } + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianDateParser.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianDateParser.java new file mode 100644 index 0000000..208699a --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/PersianDateParser.java @@ -0,0 +1,162 @@ +package mohammadaminha.com.widgets.Date_Picker.utils; + +/** + * Parses text from the beginning of the given string to produce a + * PersianCalendar. + * + *

    + * See the {@link #getPersianDate()} method for more information on date + * parsing. + * + *

    + *                Example
    + *                     
    + *  {@code
    + *    PersianCalendar pCal =
    + *     new PersianDateParser("1361/3/1").getPersianDate();             
    + *  }
    + * 
    + * + * @author Morteza contact: Mortezaadi@gmail.com + * @version 1.0 + */ +class PersianDateParser { + + private String dateString; + private String delimiter = "/"; + + /** + *
    +	 * construct parser with date string assigned
    +	 * the default delimiter is '/'.
    +	 * 
    +	 * To assign deferment delimiter use:
    +	 * {@link #PersianDateParser(String dateString, String delimiter)}
    +	 * 
    +	 *                     Example
    +	 *                     
    +	 *  {@code
    +	 *    PersianCalendar pCal =
    +	 *     new PersianDateParser("1361/3/1").getPersianDate();             
    +	 *  }
    +	 * 
    + * + * @param dateString + */ + private PersianDateParser(String dateString) { + this.dateString = dateString; + } + + /** + *
    +	 * construct parser with date string assigned
    +	 * the default delimiter is '/'. with this constructor
    +	 * you can set different delimiter to parse the date
    +	 * based on this delimiter.
    +	 * see also:
    +	 * {@link #PersianDateParser(String dateString)}
    +	 * 
    +	 *                     Example
    +	 *                     
    +	 *  {@code
    +	 *    PersianCalendar pCal =
    +	 *     new PersianDateParser("1361-3-1","-").getPersianDate();             
    +	 *  }
    +	 * 
    + * + * @param dateString + * @param delimiter + */ + public PersianDateParser(String dateString, String delimiter) { + this(dateString); + this.delimiter = delimiter; + } + + /** + * Produce the PersianCalendar object from given DateString throws Exception + * if couldn't parse the text. + * + * @return PersianCalendar object + * @exception RuntimeException + */ + public PersianCalendar getPersianDate() { + + checkDateStringInitialValidation(); + + String tokens[] = splitDateString(normalizeDateString(dateString)); + int year = Integer.parseInt(tokens[0]); + int month = Integer.parseInt(tokens[1]); + int day = Integer.parseInt(tokens[2]); + + checkPersianDateValidation(year, month, day); + + PersianCalendar pCal = new PersianCalendar(); + pCal.setPersianDate(year, month, day); + + return pCal; + } + + /** + * validate the given date + * + * @param year + * @param month + * @param day + */ + private void checkPersianDateValidation(int year, int month, int day) { + if (year < 1) + throw new RuntimeException("year is not valid"); + if (month < 1 || month > 12) + throw new RuntimeException("month is not valid"); + if (day < 1 || day > 31) + throw new RuntimeException("day is not valid"); + if (month > 6 && day == 31) + throw new RuntimeException("day is not valid"); + if (month == 12 && day == 30 && !PersianCalendarUtils.isPersianLeapYear(year)) + throw new RuntimeException("day is not valid " + year + " is not a leap year"); + } + + /** + * planned for further calculation before parsing the text + * + * @param dateString + * @return + */ + private String normalizeDateString(String dateString) { + // dateString = dateString.replace("-", delimiter); + return dateString; + } + + private String[] splitDateString(String dateString) { + String tokens[] = dateString.split(delimiter); + if (tokens.length != 3) + throw new RuntimeException("wrong date:" + dateString + " is not a Persian Date or can not be parsed"); + return tokens; + } + + private void checkDateStringInitialValidation() { + if (dateString == null) + throw new RuntimeException("input didn't assing please use setDateString()"); + // if(dateString.length()>10) + // throw new RuntimeException("wrong date:" + dateString + + // " is not a Persian Date or can not be parsed" ); + } + + public String getDateString() { + return dateString; + } + + public void setDateString(String dateString) { + this.dateString = dateString; + } + + public String getDelimiter() { + return delimiter; + } + + public void setDelimiter(String delimiter) { + this.delimiter = delimiter; + } + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/TimeZones.java b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/TimeZones.java new file mode 100644 index 0000000..30e9c86 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Date_Picker/utils/TimeZones.java @@ -0,0 +1,161 @@ +package mohammadaminha.com.widgets.Date_Picker.utils; + +import java.util.TimeZone; + +/** + * This is simply all the available TimeZones from java.util.TimeZone as type + * safe enum + */ +public enum TimeZones { + DEFAULT(TimeZone.getDefault()), + // default JDK 1.4.2 time zones + ACT(TimeZone.getTimeZone("ACT")), AET(TimeZone.getTimeZone("AET")), AFRICA_ABIDJAN(TimeZone.getTimeZone("Africa/Abidjan")), AFRICA_ACCRA(TimeZone.getTimeZone("Africa/Accra")), AFRICA_ADDIS_ABABA(TimeZone + .getTimeZone("Africa/Addis_Ababa")), AFRICA_ALGIERS(TimeZone.getTimeZone("Africa/Algiers")), AFRICA_ASMERA(TimeZone.getTimeZone("Africa/Asmera")), AFRICA_BAMAKO(TimeZone.getTimeZone("Africa/Bamako")), AFRICA_BANGUI(TimeZone + .getTimeZone("Africa/Bangui")), AFRICA_BANJUL(TimeZone.getTimeZone("Africa/Banjul")), AFRICA_BISSAU(TimeZone.getTimeZone("Africa/Bissau")), AFRICA_BLANTYRE(TimeZone.getTimeZone("Africa/Blantyre")), AFRICA_BRAZZAVILLE(TimeZone + .getTimeZone("Africa/Brazzaville")), AFRICA_BUJUMBURA(TimeZone.getTimeZone("Africa/Bujumbura")), AFRICA_CAIRO(TimeZone.getTimeZone("Africa/Cairo")), AFRICA_CASABLANCA(TimeZone.getTimeZone("Africa/Casablanca")), AFRICA_CEUTA( + TimeZone.getTimeZone("Africa/Ceuta")), AFRICA_CONAKRY(TimeZone.getTimeZone("Africa/Conakry")), AFRICA_DAKAR(TimeZone.getTimeZone("Africa/Dakar")), AFRICA_DAR_ES_SALAAM(TimeZone.getTimeZone("Africa/Dar_es_Salaam")), AFRICA_DJIBOUTI( + TimeZone.getTimeZone("Africa/Djibouti")), AFRICA_DOUALA(TimeZone.getTimeZone("Africa/Douala")), AFRICA_EL_AAIUN(TimeZone.getTimeZone("Africa/El_Aaiun")), AFRICA_FREETOWN(TimeZone.getTimeZone("Africa/Freetown")), AFRICA_GABORONE( + TimeZone.getTimeZone("Africa/Gaborone")), AFRICA_HARARE(TimeZone.getTimeZone("Africa/Harare")), AFRICA_JOHANNESBURG(TimeZone.getTimeZone("Africa/Johannesburg")), AFRICA_KAMPALA(TimeZone.getTimeZone("Africa/Kampala")), AFRICA_KHARTOUM( + TimeZone.getTimeZone("Africa/Khartoum")), AFRICA_KIGALI(TimeZone.getTimeZone("Africa/Kigali")), AFRICA_KINSHASA(TimeZone.getTimeZone("Africa/Kinshasa")), AFRICA_LAGOS(TimeZone.getTimeZone("Africa/Lagos")), AFRICA_LIBREVILLE( + TimeZone.getTimeZone("Africa/Libreville")), AFRICA_LOME(TimeZone.getTimeZone("Africa/Lome")), AFRICA_LUANDA(TimeZone.getTimeZone("Africa/Luanda")), AFRICA_LUBUMBASHI(TimeZone.getTimeZone("Africa/Lubumbashi")), AFRICA_LUSAKA( + TimeZone.getTimeZone("Africa/Lusaka")), AFRICA_MALABO(TimeZone.getTimeZone("Africa/Malabo")), AFRICA_MAPUTO(TimeZone.getTimeZone("Africa/Maputo")), AFRICA_MASERU(TimeZone.getTimeZone("Africa/Maseru")), AFRICA_MBABANE(TimeZone + .getTimeZone("Africa/Mbabane")), AFRICA_MOGADISHU(TimeZone.getTimeZone("Africa/Mogadishu")), AFRICA_MONROVIA(TimeZone.getTimeZone("Africa/Monrovia")), AFRICA_NAIROBI(TimeZone.getTimeZone("Africa/Nairobi")), AFRICA_NDJAMENA( + TimeZone.getTimeZone("Africa/Ndjamena")), AFRICA_NIAMEY(TimeZone.getTimeZone("Africa/Niamey")), AFRICA_NOUAKCHOTT(TimeZone.getTimeZone("Africa/Nouakchott")), AFRICA_OUAGADOUGOU(TimeZone.getTimeZone("Africa/Ouagadougou")), AFRICA_PORTO_NOVO( + TimeZone.getTimeZone("Africa/Porto-Novo")), AFRICA_SAO_TOME(TimeZone.getTimeZone("Africa/Sao_Tome")), AFRICA_TIMBUKTU(TimeZone.getTimeZone("Africa/Timbuktu")), AFRICA_TRIPOLI(TimeZone.getTimeZone("Africa/Tripoli")), AFRICA_TUNIS( + TimeZone.getTimeZone("Africa/Tunis")), AFRICA_WINDHOEK(TimeZone.getTimeZone("Africa/Windhoek")), AGT(TimeZone.getTimeZone("AGT")), AMERICA_ADAK(TimeZone.getTimeZone("America/Adak")), AMERICA_ANCHORAGE(TimeZone + .getTimeZone("America/Anchorage")), AMERICA_ANGUILLA(TimeZone.getTimeZone("America/Anguilla")), AMERICA_ANTIGUA(TimeZone.getTimeZone("America/Antigua")), AMERICA_ARAGUAINA(TimeZone.getTimeZone("America/Araguaina")), AMERICA_ARUBA( + TimeZone.getTimeZone("America/Aruba")), AMERICA_ASUNCION(TimeZone.getTimeZone("America/Asuncion")), AMERICA_ATKA(TimeZone.getTimeZone("America/Atka")), AMERICA_BARBADOS(TimeZone.getTimeZone("America/Barbados")), AMERICA_BELEM( + TimeZone.getTimeZone("America/Belem")), AMERICA_BELIZE(TimeZone.getTimeZone("America/Belize")), AMERICA_BOA_VISTA(TimeZone.getTimeZone("America/Boa_Vista")), AMERICA_BOGOTA(TimeZone.getTimeZone("America/Bogota")), AMERICA_BOISE( + TimeZone.getTimeZone("America/Boise")), AMERICA_BUENOS_AIRES(TimeZone.getTimeZone("America/Buenos_Aires")), AMERICA_CAMBRIDGE_BAY(TimeZone.getTimeZone("America/Cambridge_Bay")), AMERICA_CANCUN(TimeZone + .getTimeZone("America/Cancun")), AMERICA_CARACAS(TimeZone.getTimeZone("America/Caracas")), AMERICA_CATAMARCA(TimeZone.getTimeZone("America/Catamarca")), AMERICA_CAYENNE(TimeZone.getTimeZone("America/Cayenne")), AMERICA_CAYMAN( + TimeZone.getTimeZone("America/Cayman")), AMERICA_CHICAGO(TimeZone.getTimeZone("America/Chicago")), AMERICA_CHIHUAHUA(TimeZone.getTimeZone("America/Chihuahua")), AMERICA_CORDOBA(TimeZone.getTimeZone("America/Cordoba")), AMERICA_COSTA_RICA( + TimeZone.getTimeZone("America/Costa_Rica")), AMERICA_CUIABA(TimeZone.getTimeZone("America/Cuiaba")), AMERICA_CURACAO(TimeZone.getTimeZone("America/Curacao")), AMERICA_DANMARKSHAVN(TimeZone.getTimeZone("America/Danmarkshavn")), AMERICA_DAWSON( + TimeZone.getTimeZone("America/Dawson")), AMERICA_DAWSON_CREEK(TimeZone.getTimeZone("America/Dawson_Creek")), AMERICA_DENVER(TimeZone.getTimeZone("America/Denver")), AMERICA_DETROIT(TimeZone.getTimeZone("America/Detroit")), AMERICA_DOMINICA( + TimeZone.getTimeZone("America/Dominica")), AMERICA_EDMONTON(TimeZone.getTimeZone("America/Edmonton")), AMERICA_EIRUNEPE(TimeZone.getTimeZone("America/Eirunepe")), AMERICA_EL_SALVADOR(TimeZone.getTimeZone("America/El_Salvador")), AMERICA_ENSENADA( + TimeZone.getTimeZone("America/Ensenada")), AMERICA_FORT_WAYNE(TimeZone.getTimeZone("America/Fort_Wayne")), AMERICA_FORTALEZA(TimeZone.getTimeZone("America/Fortaleza")), AMERICA_GLACE_BAY(TimeZone + .getTimeZone("America/Glace_Bay")), AMERICA_GODTHAB(TimeZone.getTimeZone("America/Godthab")), AMERICA_GOOSE_BAY(TimeZone.getTimeZone("America/Goose_Bay")), AMERICA_GRAND_TURK(TimeZone.getTimeZone("America/Grand_Turk")), AMERICA_GRENADA( + TimeZone.getTimeZone("America/Grenada")), AMERICA_GUADELOUPE(TimeZone.getTimeZone("America/Guadeloupe")), AMERICA_GUATEMALA(TimeZone.getTimeZone("America/Guatemala")), AMERICA_GUAYAQUIL(TimeZone.getTimeZone("America/Guayaquil")), AMERICA_GUYANA( + TimeZone.getTimeZone("America/Guyana")), AMERICA_HALIFAX(TimeZone.getTimeZone("America/Halifax")), AMERICA_HAVANA(TimeZone.getTimeZone("America/Havana")), AMERICA_HERMOSILLO(TimeZone.getTimeZone("America/Hermosillo")), AMERICA_INDIANA_INDIANAPOLIS( + TimeZone.getTimeZone("America/Indiana/Indianapolis")), AMERICA_INDIANA_KNOX(TimeZone.getTimeZone("America/Indiana/Knox")), AMERICA_INDIANA_MARENGO(TimeZone.getTimeZone("America/Indiana/Marengo")), AMERICA_INDIANA_VEVAY(TimeZone + .getTimeZone("America/Indiana/Vevay")), AMERICA_INDIANAPOLIS(TimeZone.getTimeZone("America/Indianapolis")), AMERICA_INUVIK(TimeZone.getTimeZone("America/Inuvik")), AMERICA_IQALUIT(TimeZone.getTimeZone("America/Iqaluit")), AMERICA_JAMAICA( + TimeZone.getTimeZone("America/Jamaica")), AMERICA_JUJUY(TimeZone.getTimeZone("America/Jujuy")), AMERICA_JUNEAU(TimeZone.getTimeZone("America/Juneau")), AMERICA_KENTUCKY_LOUISVILLE(TimeZone + .getTimeZone("America/Kentucky/Louisville")), AMERICA_KENTUCKY_MONTICELLO(TimeZone.getTimeZone("America/Kentucky/Monticello")), AMERICA_KNOX_IN(TimeZone.getTimeZone("America/Knox_IN")), AMERICA_LA_PAZ(TimeZone + .getTimeZone("America/La_Paz")), AMERICA_LIMA(TimeZone.getTimeZone("America/Lima")), AMERICA_LOS_ANGELES(TimeZone.getTimeZone("America/Los_Angeles")), AMERICA_LOUISVILLE(TimeZone.getTimeZone("America/Louisville")), AMERICA_MACEIO( + TimeZone.getTimeZone("America/Maceio")), AMERICA_MANAGUA(TimeZone.getTimeZone("America/Managua")), AMERICA_MANAUS(TimeZone.getTimeZone("America/Manaus")), AMERICA_MARTINIQUE(TimeZone.getTimeZone("America/Martinique")), AMERICA_MAZATLAN( + TimeZone.getTimeZone("America/Mazatlan")), AMERICA_MENDOZA(TimeZone.getTimeZone("America/Mendoza")), AMERICA_MENOMINEE(TimeZone.getTimeZone("America/Menominee")), AMERICA_MERIDA(TimeZone.getTimeZone("America/Merida")), AMERICA_MEXICO_CITY( + TimeZone.getTimeZone("America/Mexico_City")), AMERICA_MIQUELON(TimeZone.getTimeZone("America/Miquelon")), AMERICA_MONTERREY(TimeZone.getTimeZone("America/Monterrey")), AMERICA_MONTEVIDEO(TimeZone + .getTimeZone("America/Montevideo")), AMERICA_MONTREAL(TimeZone.getTimeZone("America/Montreal")), AMERICA_MONTSERRAT(TimeZone.getTimeZone("America/Montserrat")), AMERICA_NASSAU(TimeZone.getTimeZone("America/Nassau")), AMERICA_NEW_YORK( + TimeZone.getTimeZone("America/New_York")), AMERICA_NIPIGON(TimeZone.getTimeZone("America/Nipigon")), AMERICA_NOME(TimeZone.getTimeZone("America/Nome")), AMERICA_NORONHA(TimeZone.getTimeZone("America/Noronha")), AMERICA_NORTH_DAKOTA_CENTER( + TimeZone.getTimeZone("America/North_Dakota/Center")), AMERICA_PANAMA(TimeZone.getTimeZone("America/Panama")), AMERICA_PANGNIRTUNG(TimeZone.getTimeZone("America/Pangnirtung")), AMERICA_PARAMARIBO(TimeZone + .getTimeZone("America/Paramaribo")), AMERICA_PHOENIX(TimeZone.getTimeZone("America/Phoenix")), AMERICA_PORT_AU_PRINCE(TimeZone.getTimeZone("America/Port-au-Prince")), AMERICA_PORT_OF_SPAIN(TimeZone + .getTimeZone("America/Port_of_Spain")), AMERICA_PORTO_ACRE(TimeZone.getTimeZone("America/Porto_Acre")), AMERICA_PORTO_VELHO(TimeZone.getTimeZone("America/Porto_Velho")), AMERICA_PUERTO_RICO(TimeZone + .getTimeZone("America/Puerto_Rico")), AMERICA_RAINY_RIVER(TimeZone.getTimeZone("America/Rainy_River")), AMERICA_RANKIN_INLET(TimeZone.getTimeZone("America/Rankin_Inlet")), AMERICA_RECIFE(TimeZone.getTimeZone("America/Recife")), AMERICA_REGINA( + TimeZone.getTimeZone("America/Regina")), AMERICA_RIO_BRANCO(TimeZone.getTimeZone("America/Rio_Branco")), AMERICA_ROSARIO(TimeZone.getTimeZone("America/Rosario")), AMERICA_SANTIAGO(TimeZone.getTimeZone("America/Santiago")), AMERICA_SANTO_DOMINGO( + TimeZone.getTimeZone("America/Santo_Domingo")), AMERICA_SAO_PAULO(TimeZone.getTimeZone("America/Sao_Paulo")), AMERICA_SCORESBYSUND(TimeZone.getTimeZone("America/Scoresbysund")), AMERICA_SHIPROCK(TimeZone + .getTimeZone("America/Shiprock")), AMERICA_ST_JOHNS(TimeZone.getTimeZone("America/St_Johns")), AMERICA_ST_KITTS(TimeZone.getTimeZone("America/St_Kitts")), AMERICA_ST_LUCIA(TimeZone.getTimeZone("America/St_Lucia")), AMERICA_ST_THOMAS( + TimeZone.getTimeZone("America/St_Thomas")), AMERICA_ST_VINCENT(TimeZone.getTimeZone("America/St_Vincent")), AMERICA_SWIFT_CURRENT(TimeZone.getTimeZone("America/Swift_Current")), AMERICA_TEGUCIGALPA(TimeZone + .getTimeZone("America/Tegucigalpa")), AMERICA_THULE(TimeZone.getTimeZone("America/Thule")), AMERICA_THUNDER_BAY(TimeZone.getTimeZone("America/Thunder_Bay")), AMERICA_TIJUANA(TimeZone.getTimeZone("America/Tijuana")), AMERICA_TORTOLA( + TimeZone.getTimeZone("America/Tortola")), AMERICA_VANCOUVER(TimeZone.getTimeZone("America/Vancouver")), AMERICA_VIRGIN(TimeZone.getTimeZone("America/Virgin")), AMERICA_WHITEHORSE(TimeZone.getTimeZone("America/Whitehorse")), AMERICA_WINNIPEG( + TimeZone.getTimeZone("America/Winnipeg")), AMERICA_YAKUTAT(TimeZone.getTimeZone("America/Yakutat")), AMERICA_YELLOWKNIFE(TimeZone.getTimeZone("America/Yellowknife")), ANTARCTICA_CASEY(TimeZone.getTimeZone("Antarctica/Casey")), ANTARCTICA_DAVIS( + TimeZone.getTimeZone("Antarctica/Davis")), ANTARCTICA_DUMONTDURVILLE(TimeZone.getTimeZone("Antarctica/DumontDUrville")), ANTARCTICA_MAWSON(TimeZone.getTimeZone("Antarctica/Mawson")), ANTARCTICA_MCMURDO(TimeZone + .getTimeZone("Antarctica/McMurdo")), ANTARCTICA_PALMER(TimeZone.getTimeZone("Antarctica/Palmer")), ANTARCTICA_ROTHERA(TimeZone.getTimeZone("Antarctica/Rothera")), ANTARCTICA_SOUTH_POLE(TimeZone + .getTimeZone("Antarctica/South_Pole")), ANTARCTICA_SYOWA(TimeZone.getTimeZone("Antarctica/Syowa")), ANTARCTICA_VOSTOK(TimeZone.getTimeZone("Antarctica/Vostok")), ARCTIC_LONGYEARBYEN(TimeZone.getTimeZone("Arctic/Longyearbyen")), ART( + TimeZone.getTimeZone("ART")), ASIA_ADEN(TimeZone.getTimeZone("Asia/Aden")), ASIA_ALMATY(TimeZone.getTimeZone("Asia/Almaty")), ASIA_AMMAN(TimeZone.getTimeZone("Asia/Amman")), ASIA_ANADYR(TimeZone.getTimeZone("Asia/Anadyr")), ASIA_AQTAU( + TimeZone.getTimeZone("Asia/Aqtau")), ASIA_AQTOBE(TimeZone.getTimeZone("Asia/Aqtobe")), ASIA_ASHGABAT(TimeZone.getTimeZone("Asia/Ashgabat")), ASIA_ASHKHABAD(TimeZone.getTimeZone("Asia/Ashkhabad")), ASIA_BAGHDAD(TimeZone + .getTimeZone("Asia/Baghdad")), ASIA_BAHRAIN(TimeZone.getTimeZone("Asia/Bahrain")), ASIA_BAKU(TimeZone.getTimeZone("Asia/Baku")), ASIA_BANGKOK(TimeZone.getTimeZone("Asia/Bangkok")), ASIA_BEIRUT(TimeZone + .getTimeZone("Asia/Beirut")), ASIA_BISHKEK(TimeZone.getTimeZone("Asia/Bishkek")), ASIA_BRUNEI(TimeZone.getTimeZone("Asia/Brunei")), ASIA_CALCUTTA(TimeZone.getTimeZone("Asia/Calcutta")), ASIA_CHOIBALSAN(TimeZone + .getTimeZone("Asia/Choibalsan")), ASIA_CHONGQING(TimeZone.getTimeZone("Asia/Chongqing")), ASIA_CHUNGKING(TimeZone.getTimeZone("Asia/Chungking")), ASIA_COLOMBO(TimeZone.getTimeZone("Asia/Colombo")), ASIA_DACCA(TimeZone + .getTimeZone("Asia/Dacca")), ASIA_DAMASCUS(TimeZone.getTimeZone("Asia/Damascus")), ASIA_DHAKA(TimeZone.getTimeZone("Asia/Dhaka")), ASIA_DILI(TimeZone.getTimeZone("Asia/Dili")), ASIA_DUBAI(TimeZone.getTimeZone("Asia/Dubai")), ASIA_DUSHANBE( + TimeZone.getTimeZone("Asia/Dushanbe")), ASIA_GAZA(TimeZone.getTimeZone("Asia/Gaza")), ASIA_HARBIN(TimeZone.getTimeZone("Asia/Harbin")), ASIA_HONG_KONG(TimeZone.getTimeZone("Asia/Hong_Kong")), ASIA_HOVD(TimeZone + .getTimeZone("Asia/Hovd")), ASIA_IRKUTSK(TimeZone.getTimeZone("Asia/Irkutsk")), ASIA_ISTANBUL(TimeZone.getTimeZone("Asia/Istanbul")), ASIA_JAKARTA(TimeZone.getTimeZone("Asia/Jakarta")), ASIA_JAYAPURA(TimeZone + .getTimeZone("Asia/Jayapura")), ASIA_JERUSALEM(TimeZone.getTimeZone("Asia/Jerusalem")), ASIA_KABUL(TimeZone.getTimeZone("Asia/Kabul")), ASIA_KAMCHATKA(TimeZone.getTimeZone("Asia/Kamchatka")), ASIA_KARACHI(TimeZone + .getTimeZone("Asia/Karachi")), ASIA_KASHGAR(TimeZone.getTimeZone("Asia/Kashgar")), ASIA_KATMANDU(TimeZone.getTimeZone("Asia/Katmandu")), ASIA_KRASNOYARSK(TimeZone.getTimeZone("Asia/Krasnoyarsk")), ASIA_KUALA_LUMPUR(TimeZone + .getTimeZone("Asia/Kuala_Lumpur")), ASIA_KUCHING(TimeZone.getTimeZone("Asia/Kuching")), ASIA_KUWAIT(TimeZone.getTimeZone("Asia/Kuwait")), ASIA_MACAO(TimeZone.getTimeZone("Asia/Macao")), ASIA_MACAU(TimeZone + .getTimeZone("Asia/Macau")), ASIA_MAGADAN(TimeZone.getTimeZone("Asia/Magadan")), ASIA_MAKASSAR(TimeZone.getTimeZone("Asia/Makassar")), ASIA_MANILA(TimeZone.getTimeZone("Asia/Manila")), ASIA_MUSCAT(TimeZone + .getTimeZone("Asia/Muscat")), ASIA_NICOSIA(TimeZone.getTimeZone("Asia/Nicosia")), ASIA_NOVOSIBIRSK(TimeZone.getTimeZone("Asia/Novosibirsk")), ASIA_OMSK(TimeZone.getTimeZone("Asia/Omsk")), ASIA_ORAL(TimeZone + .getTimeZone("Asia/Oral")), ASIA_PHNOM_PENH(TimeZone.getTimeZone("Asia/Phnom_Penh")), ASIA_PONTIANAK(TimeZone.getTimeZone("Asia/Pontianak")), ASIA_PYONGYANG(TimeZone.getTimeZone("Asia/Pyongyang")), ASIA_QATAR(TimeZone + .getTimeZone("Asia/Qatar")), ASIA_QYZYLORDA(TimeZone.getTimeZone("Asia/Qyzylorda")), ASIA_RANGOON(TimeZone.getTimeZone("Asia/Rangoon")), ASIA_RIYADH(TimeZone.getTimeZone("Asia/Riyadh")), ASIA_RIYADH87(TimeZone + .getTimeZone("Asia/Riyadh87")), ASIA_RIYADH88(TimeZone.getTimeZone("Asia/Riyadh88")), ASIA_RIYADH89(TimeZone.getTimeZone("Asia/Riyadh89")), ASIA_SAIGON(TimeZone.getTimeZone("Asia/Saigon")), ASIA_SAKHALIN(TimeZone + .getTimeZone("Asia/Sakhalin")), ASIA_SAMARKAND(TimeZone.getTimeZone("Asia/Samarkand")), ASIA_SEOUL(TimeZone.getTimeZone("Asia/Seoul")), ASIA_SHANGHAI(TimeZone.getTimeZone("Asia/Shanghai")), ASIA_SINGAPORE(TimeZone + .getTimeZone("Asia/Singapore")), ASIA_TAIPEI(TimeZone.getTimeZone("Asia/Taipei")), ASIA_TASHKENT(TimeZone.getTimeZone("Asia/Tashkent")), ASIA_TBILISI(TimeZone.getTimeZone("Asia/Tbilisi")), ASIA_TEHRAN(TimeZone + .getTimeZone("Asia/Tehran")), ASIA_TEL_AVIV(TimeZone.getTimeZone("Asia/Tel_Aviv")), ASIA_THIMBU(TimeZone.getTimeZone("Asia/Thimbu")), ASIA_THIMPHU(TimeZone.getTimeZone("Asia/Thimphu")), ASIA_TOKYO(TimeZone + .getTimeZone("Asia/Tokyo")), ASIA_UJUNG_PANDANG(TimeZone.getTimeZone("Asia/Ujung_Pandang")), ASIA_ULAANBAATAR(TimeZone.getTimeZone("Asia/Ulaanbaatar")), ASIA_ULAN_BATOR(TimeZone.getTimeZone("Asia/Ulan_Bator")), ASIA_URUMQI( + TimeZone.getTimeZone("Asia/Urumqi")), ASIA_VIENTIANE(TimeZone.getTimeZone("Asia/Vientiane")), ASIA_VLADIVOSTOK(TimeZone.getTimeZone("Asia/Vladivostok")), ASIA_YAKUTSK(TimeZone.getTimeZone("Asia/Yakutsk")), ASIA_YEKATERINBURG( + TimeZone.getTimeZone("Asia/Yekaterinburg")), ASIA_YEREVAN(TimeZone.getTimeZone("Asia/Yerevan")), AST(TimeZone.getTimeZone("AST")), ATLANTIC_AZORES(TimeZone.getTimeZone("Atlantic/Azores")), ATLANTIC_BERMUDA(TimeZone + .getTimeZone("Atlantic/Bermuda")), ATLANTIC_CANARY(TimeZone.getTimeZone("Atlantic/Canary")), ATLANTIC_CAPE_VERDE(TimeZone.getTimeZone("Atlantic/Cape_Verde")), ATLANTIC_FAEROE(TimeZone.getTimeZone("Atlantic/Faeroe")), ATLANTIC_JAN_MAYEN( + TimeZone.getTimeZone("Atlantic/Jan_Mayen")), ATLANTIC_MADEIRA(TimeZone.getTimeZone("Atlantic/Madeira")), ATLANTIC_REYKJAVIK(TimeZone.getTimeZone("Atlantic/Reykjavik")), ATLANTIC_SOUTH_GEORGIA(TimeZone + .getTimeZone("Atlantic/South_Georgia")), ATLANTIC_ST_HELENA(TimeZone.getTimeZone("Atlantic/St_Helena")), ATLANTIC_STANLEY(TimeZone.getTimeZone("Atlantic/Stanley")), AUSTRALIA_ACT(TimeZone.getTimeZone("Australia/ACT")), AUSTRALIA_ADELAIDE( + TimeZone.getTimeZone("Australia/Adelaide")), AUSTRALIA_BRISBANE(TimeZone.getTimeZone("Australia/Brisbane")), AUSTRALIA_BROKEN_HILL(TimeZone.getTimeZone("Australia/Broken_Hill")), AUSTRALIA_CANBERRA(TimeZone + .getTimeZone("Australia/Canberra")), AUSTRALIA_DARWIN(TimeZone.getTimeZone("Australia/Darwin")), AUSTRALIA_HOBART(TimeZone.getTimeZone("Australia/Hobart")), AUSTRALIA_LHI(TimeZone.getTimeZone("Australia/LHI")), AUSTRALIA_LINDEMAN( + TimeZone.getTimeZone("Australia/Lindeman")), AUSTRALIA_LORD_HOWE(TimeZone.getTimeZone("Australia/Lord_Howe")), AUSTRALIA_MELBOURNE(TimeZone.getTimeZone("Australia/Melbourne")), AUSTRALIA_NORTH(TimeZone + .getTimeZone("Australia/North")), AUSTRALIA_NSW(TimeZone.getTimeZone("Australia/NSW")), AUSTRALIA_PERTH(TimeZone.getTimeZone("Australia/Perth")), AUSTRALIA_QUEENSLAND(TimeZone.getTimeZone("Australia/Queensland")), AUSTRALIA_SOUTH( + TimeZone.getTimeZone("Australia/South")), AUSTRALIA_SYDNEY(TimeZone.getTimeZone("Australia/Sydney")), AUSTRALIA_TASMANIA(TimeZone.getTimeZone("Australia/Tasmania")), AUSTRALIA_VICTORIA(TimeZone.getTimeZone("Australia/Victoria")), AUSTRALIA_WEST( + TimeZone.getTimeZone("Australia/West")), AUSTRALIA_YANCOWINNA(TimeZone.getTimeZone("Australia/Yancowinna")), BET(TimeZone.getTimeZone("BET")), BRAZIL_ACRE(TimeZone.getTimeZone("Brazil/Acre")), BRAZIL_DENORONHA(TimeZone + .getTimeZone("Brazil/DeNoronha")), BRAZIL_EAST(TimeZone.getTimeZone("Brazil/East")), BRAZIL_WEST(TimeZone.getTimeZone("Brazil/West")), BST(TimeZone.getTimeZone("BST")), CANADA_ATLANTIC(TimeZone.getTimeZone("Canada/Atlantic")), CANADA_CENTRAL( + TimeZone.getTimeZone("Canada/Central")), CANADA_EAST_SASKATCHEWAN(TimeZone.getTimeZone("Canada/East-Saskatchewan")), CANADA_EASTERN(TimeZone.getTimeZone("Canada/Eastern")), CANADA_MOUNTAIN(TimeZone + .getTimeZone("Canada/Mountain")), CANADA_NEWFOUNDLAND(TimeZone.getTimeZone("Canada/Newfoundland")), CANADA_PACIFIC(TimeZone.getTimeZone("Canada/Pacific")), CANADA_SASKATCHEWAN(TimeZone.getTimeZone("Canada/Saskatchewan")), CANADA_YUKON( + TimeZone.getTimeZone("Canada/Yukon")), CAT(TimeZone.getTimeZone("CAT")), CET(TimeZone.getTimeZone("CET")), CHILE_CONTINENTAL(TimeZone.getTimeZone("Chile/Continental")), CHILE_EASTERISLAND(TimeZone + .getTimeZone("Chile/EasterIsland")), CNT(TimeZone.getTimeZone("CNT")), CST(TimeZone.getTimeZone("CST")), CST6CDT(TimeZone.getTimeZone("CST6CDT")), CTT(TimeZone.getTimeZone("CTT")), CUBA(TimeZone.getTimeZone("Cuba")), EAT( + TimeZone.getTimeZone("EAT")), ECT(TimeZone.getTimeZone("ECT")), EET(TimeZone.getTimeZone("EET")), EGYPT(TimeZone.getTimeZone("Egypt")), EIRE(TimeZone.getTimeZone("Eire")), EST(TimeZone.getTimeZone("EST")), EST5EDT(TimeZone + .getTimeZone("EST5EDT")), ETC_GMT(TimeZone.getTimeZone("Etc/GMT")), ETC_GMT_PLUS_0(TimeZone.getTimeZone("Etc/GMT+0")), ETC_GMT_PLUS_1(TimeZone.getTimeZone("Etc/GMT+1")), ETC_GMT_PLUS_10(TimeZone.getTimeZone("Etc/GMT+10")), ETC_GMT_PLUS_11( + TimeZone.getTimeZone("Etc/GMT+11")), ETC_GMT_PLUS_12(TimeZone.getTimeZone("Etc/GMT+12")), ETC_GMT_PLUS_2(TimeZone.getTimeZone("Etc/GMT+2")), ETC_GMT_PLUS_3(TimeZone.getTimeZone("Etc/GMT+3")), ETC_GMT_PLUS_4(TimeZone + .getTimeZone("Etc/GMT+4")), ETC_GMT_PLUS_5(TimeZone.getTimeZone("Etc/GMT+5")), ETC_GMT_PLUS_6(TimeZone.getTimeZone("Etc/GMT+6")), ETC_GMT_PLUS_7(TimeZone.getTimeZone("Etc/GMT+7")), ETC_GMT_PLUS_8(TimeZone + .getTimeZone("Etc/GMT+8")), ETC_GMT_PLUS_9(TimeZone.getTimeZone("Etc/GMT+9")), ETC_GMT_MINUS_0(TimeZone.getTimeZone("Etc/GMT-0")), ETC_GMT_MINUS_1(TimeZone.getTimeZone("Etc/GMT-1")), ETC_GMT_MINUS_10(TimeZone + .getTimeZone("Etc/GMT-10")), ETC_GMT_MINUS_11(TimeZone.getTimeZone("Etc/GMT-11")), ETC_GMT_MINUS_12(TimeZone.getTimeZone("Etc/GMT-12")), ETC_GMT_MINUS_13(TimeZone.getTimeZone("Etc/GMT-13")), ETC_GMT_MINUS_14(TimeZone + .getTimeZone("Etc/GMT-14")), ETC_GMT_MINUS_2(TimeZone.getTimeZone("Etc/GMT-2")), ETC_GMT_MINUS_3(TimeZone.getTimeZone("Etc/GMT-3")), ETC_GMT_MINUS_4(TimeZone.getTimeZone("Etc/GMT-4")), ETC_GMT_MINUS_5(TimeZone + .getTimeZone("Etc/GMT-5")), ETC_GMT_MINUS_6(TimeZone.getTimeZone("Etc/GMT-6")), ETC_GMT_MINUS_7(TimeZone.getTimeZone("Etc/GMT-7")), ETC_GMT_MINUS_8(TimeZone.getTimeZone("Etc/GMT-8")), ETC_GMT_MINUS_9(TimeZone + .getTimeZone("Etc/GMT-9")), ETC_GMT0(TimeZone.getTimeZone("Etc/GMT0")), ETC_GREENWICH(TimeZone.getTimeZone("Etc/Greenwich")), ETC_UCT(TimeZone.getTimeZone("Etc/UCT")), ETC_UNIVERSAL(TimeZone.getTimeZone("Etc/Universal")), ETC_UTC( + TimeZone.getTimeZone("Etc/UTC")), ETC_ZULU(TimeZone.getTimeZone("Etc/Zulu")), EUROPE_AMSTERDAM(TimeZone.getTimeZone("Europe/Amsterdam")), EUROPE_ANDORRA(TimeZone.getTimeZone("Europe/Andorra")), EUROPE_ATHENS(TimeZone + .getTimeZone("Europe/Athens")), EUROPE_BELFAST(TimeZone.getTimeZone("Europe/Belfast")), EUROPE_BELGRADE(TimeZone.getTimeZone("Europe/Belgrade")), EUROPE_BERLIN(TimeZone.getTimeZone("Europe/Berlin")), EUROPE_BRATISLAVA(TimeZone + .getTimeZone("Europe/Bratislava")), EUROPE_BRUSSELS(TimeZone.getTimeZone("Europe/Brussels")), EUROPE_BUCHAREST(TimeZone.getTimeZone("Europe/Bucharest")), EUROPE_BUDAPEST(TimeZone.getTimeZone("Europe/Budapest")), EUROPE_CHISINAU( + TimeZone.getTimeZone("Europe/Chisinau")), EUROPE_COPENHAGEN(TimeZone.getTimeZone("Europe/Copenhagen")), EUROPE_DUBLIN(TimeZone.getTimeZone("Europe/Dublin")), EUROPE_GIBRALTAR(TimeZone.getTimeZone("Europe/Gibraltar")), EUROPE_HELSINKI( + TimeZone.getTimeZone("Europe/Helsinki")), EUROPE_ISTANBUL(TimeZone.getTimeZone("Europe/Istanbul")), EUROPE_KALININGRAD(TimeZone.getTimeZone("Europe/Kaliningrad")), EUROPE_KIEV(TimeZone.getTimeZone("Europe/Kiev")), EUROPE_LISBON( + TimeZone.getTimeZone("Europe/Lisbon")), EUROPE_LJUBLJANA(TimeZone.getTimeZone("Europe/Ljubljana")), EUROPE_LONDON(TimeZone.getTimeZone("Europe/London")), EUROPE_LUXEMBOURG(TimeZone.getTimeZone("Europe/Luxembourg")), EUROPE_MADRID( + TimeZone.getTimeZone("Europe/Madrid")), EUROPE_MALTA(TimeZone.getTimeZone("Europe/Malta")), EUROPE_MINSK(TimeZone.getTimeZone("Europe/Minsk")), EUROPE_MONACO(TimeZone.getTimeZone("Europe/Monaco")), EUROPE_MOSCOW(TimeZone + .getTimeZone("Europe/Moscow")), EUROPE_NICOSIA(TimeZone.getTimeZone("Europe/Nicosia")), EUROPE_OSLO(TimeZone.getTimeZone("Europe/Oslo")), EUROPE_PARIS(TimeZone.getTimeZone("Europe/Paris")), EUROPE_PRAGUE(TimeZone + .getTimeZone("Europe/Prague")), EUROPE_RIGA(TimeZone.getTimeZone("Europe/Riga")), EUROPE_ROME(TimeZone.getTimeZone("Europe/Rome")), EUROPE_SAMARA(TimeZone.getTimeZone("Europe/Samara")), EUROPE_SAN_MARINO(TimeZone + .getTimeZone("Europe/San_Marino")), EUROPE_SARAJEVO(TimeZone.getTimeZone("Europe/Sarajevo")), EUROPE_SIMFEROPOL(TimeZone.getTimeZone("Europe/Simferopol")), EUROPE_SKOPJE(TimeZone.getTimeZone("Europe/Skopje")), EUROPE_SOFIA( + TimeZone.getTimeZone("Europe/Sofia")), EUROPE_STOCKHOLM(TimeZone.getTimeZone("Europe/Stockholm")), EUROPE_TALLINN(TimeZone.getTimeZone("Europe/Tallinn")), EUROPE_TIRANE(TimeZone.getTimeZone("Europe/Tirane")), EUROPE_TIRASPOL( + TimeZone.getTimeZone("Europe/Tiraspol")), EUROPE_UZHGOROD(TimeZone.getTimeZone("Europe/Uzhgorod")), EUROPE_VADUZ(TimeZone.getTimeZone("Europe/Vaduz")), EUROPE_VATICAN(TimeZone.getTimeZone("Europe/Vatican")), EUROPE_VIENNA( + TimeZone.getTimeZone("Europe/Vienna")), EUROPE_VILNIUS(TimeZone.getTimeZone("Europe/Vilnius")), EUROPE_WARSAW(TimeZone.getTimeZone("Europe/Warsaw")), EUROPE_ZAGREB(TimeZone.getTimeZone("Europe/Zagreb")), EUROPE_ZAPOROZHYE( + TimeZone.getTimeZone("Europe/Zaporozhye")), EUROPE_ZURICH(TimeZone.getTimeZone("Europe/Zurich")), GB(TimeZone.getTimeZone("GB")), GB_EIRE(TimeZone.getTimeZone("GB-Eire")), GMT(TimeZone.getTimeZone("GMT")), GMT0(TimeZone + .getTimeZone("GMT0")), GREENWICH(TimeZone.getTimeZone("Greenwich")), HONGKONG(TimeZone.getTimeZone("Hongkong")), HST(TimeZone.getTimeZone("HST")), ICELAND(TimeZone.getTimeZone("Iceland")), IET(TimeZone.getTimeZone("IET")), INDIAN_ANTANANARIVO( + TimeZone.getTimeZone("Indian/Antananarivo")), INDIAN_CHAGOS(TimeZone.getTimeZone("Indian/Chagos")), INDIAN_CHRISTMAS(TimeZone.getTimeZone("Indian/Christmas")), INDIAN_COCOS(TimeZone.getTimeZone("Indian/Cocos")), INDIAN_COMORO( + TimeZone.getTimeZone("Indian/Comoro")), INDIAN_KERGUELEN(TimeZone.getTimeZone("Indian/Kerguelen")), INDIAN_MAHE(TimeZone.getTimeZone("Indian/Mahe")), INDIAN_MALDIVES(TimeZone.getTimeZone("Indian/Maldives")), INDIAN_MAURITIUS( + TimeZone.getTimeZone("Indian/Mauritius")), INDIAN_MAYOTTE(TimeZone.getTimeZone("Indian/Mayotte")), INDIAN_REUNION(TimeZone.getTimeZone("Indian/Reunion")), IRAN(TimeZone.getTimeZone("Iran")), ISRAEL(TimeZone + .getTimeZone("Israel")), IST(TimeZone.getTimeZone("IST")), JAMAICA(TimeZone.getTimeZone("Jamaica")), JAPAN(TimeZone.getTimeZone("Japan")), JST(TimeZone.getTimeZone("JST")), KWAJALEIN(TimeZone.getTimeZone("Kwajalein")), LIBYA( + TimeZone.getTimeZone("Libya")), MET(TimeZone.getTimeZone("MET")), MEXICO_BAJANORTE(TimeZone.getTimeZone("Mexico/BajaNorte")), MEXICO_BAJASUR(TimeZone.getTimeZone("Mexico/BajaSur")), MEXICO_GENERAL(TimeZone + .getTimeZone("Mexico/General")), MIDEAST_RIYADH87(TimeZone.getTimeZone("Mideast/Riyadh87")), MIDEAST_RIYADH88(TimeZone.getTimeZone("Mideast/Riyadh88")), MIDEAST_RIYADH89(TimeZone.getTimeZone("Mideast/Riyadh89")), MIT(TimeZone + .getTimeZone("MIT")), MST(TimeZone.getTimeZone("MST")), MST7MDT(TimeZone.getTimeZone("MST7MDT")), NAVAJO(TimeZone.getTimeZone("Navajo")), NET(TimeZone.getTimeZone("NET")), NST(TimeZone.getTimeZone("NST")), NZ(TimeZone + .getTimeZone("NZ")), NZ_CHAT(TimeZone.getTimeZone("NZ-CHAT")), PACIFIC_APIA(TimeZone.getTimeZone("Pacific/Apia")), PACIFIC_AUCKLAND(TimeZone.getTimeZone("Pacific/Auckland")), PACIFIC_CHATHAM(TimeZone + .getTimeZone("Pacific/Chatham")), PACIFIC_EASTER(TimeZone.getTimeZone("Pacific/Easter")), PACIFIC_EFATE(TimeZone.getTimeZone("Pacific/Efate")), PACIFIC_ENDERBURY(TimeZone.getTimeZone("Pacific/Enderbury")), PACIFIC_FAKAOFO( + TimeZone.getTimeZone("Pacific/Fakaofo")), PACIFIC_FIJI(TimeZone.getTimeZone("Pacific/Fiji")), PACIFIC_FUNAFUTI(TimeZone.getTimeZone("Pacific/Funafuti")), PACIFIC_GALAPAGOS(TimeZone.getTimeZone("Pacific/Galapagos")), PACIFIC_GAMBIER( + TimeZone.getTimeZone("Pacific/Gambier")), PACIFIC_GUADALCANAL(TimeZone.getTimeZone("Pacific/Guadalcanal")), PACIFIC_GUAM(TimeZone.getTimeZone("Pacific/Guam")), PACIFIC_HONOLULU(TimeZone.getTimeZone("Pacific/Honolulu")), PACIFIC_JOHNSTON( + TimeZone.getTimeZone("Pacific/Johnston")), PACIFIC_KIRITIMATI(TimeZone.getTimeZone("Pacific/Kiritimati")), PACIFIC_KOSRAE(TimeZone.getTimeZone("Pacific/Kosrae")), PACIFIC_KWAJALEIN(TimeZone.getTimeZone("Pacific/Kwajalein")), PACIFIC_MAJURO( + TimeZone.getTimeZone("Pacific/Majuro")), PACIFIC_MARQUESAS(TimeZone.getTimeZone("Pacific/Marquesas")), PACIFIC_MIDWAY(TimeZone.getTimeZone("Pacific/Midway")), PACIFIC_NAURU(TimeZone.getTimeZone("Pacific/Nauru")), PACIFIC_NIUE( + TimeZone.getTimeZone("Pacific/Niue")), PACIFIC_NORFOLK(TimeZone.getTimeZone("Pacific/Norfolk")), PACIFIC_NOUMEA(TimeZone.getTimeZone("Pacific/Noumea")), PACIFIC_PAGO_PAGO(TimeZone.getTimeZone("Pacific/Pago_Pago")), PACIFIC_PALAU( + TimeZone.getTimeZone("Pacific/Palau")), PACIFIC_PITCAIRN(TimeZone.getTimeZone("Pacific/Pitcairn")), PACIFIC_PONAPE(TimeZone.getTimeZone("Pacific/Ponape")), PACIFIC_PORT_MORESBY(TimeZone.getTimeZone("Pacific/Port_Moresby")), PACIFIC_RAROTONGA( + TimeZone.getTimeZone("Pacific/Rarotonga")), PACIFIC_SAIPAN(TimeZone.getTimeZone("Pacific/Saipan")), PACIFIC_SAMOA(TimeZone.getTimeZone("Pacific/Samoa")), PACIFIC_TAHITI(TimeZone.getTimeZone("Pacific/Tahiti")), PACIFIC_TARAWA( + TimeZone.getTimeZone("Pacific/Tarawa")), PACIFIC_TONGATAPU(TimeZone.getTimeZone("Pacific/Tongatapu")), PACIFIC_TRUK(TimeZone.getTimeZone("Pacific/Truk")), PACIFIC_WAKE(TimeZone.getTimeZone("Pacific/Wake")), PACIFIC_WALLIS( + TimeZone.getTimeZone("Pacific/Wallis")), PACIFIC_YAP(TimeZone.getTimeZone("Pacific/Yap")), PLT(TimeZone.getTimeZone("PLT")), PNT(TimeZone.getTimeZone("PNT")), POLAND(TimeZone.getTimeZone("Poland")), PORTUGAL(TimeZone + .getTimeZone("Portugal")), PRC(TimeZone.getTimeZone("PRC")), PRT(TimeZone.getTimeZone("PRT")), PST(TimeZone.getTimeZone("PST")), PST8PDT(TimeZone.getTimeZone("PST8PDT")), ROK(TimeZone.getTimeZone("ROK")), SINGAPORE(TimeZone + .getTimeZone("Singapore")), SST(TimeZone.getTimeZone("SST")), SYSTEMV_AST4(TimeZone.getTimeZone("SystemV/AST4")), SYSTEMV_AST4ADT(TimeZone.getTimeZone("SystemV/AST4ADT")), SYSTEMV_CST6(TimeZone.getTimeZone("SystemV/CST6")), SYSTEMV_CST6CDT( + TimeZone.getTimeZone("SystemV/CST6CDT")), SYSTEMV_EST5(TimeZone.getTimeZone("SystemV/EST5")), SYSTEMV_EST5EDT(TimeZone.getTimeZone("SystemV/EST5EDT")), SYSTEMV_HST10(TimeZone.getTimeZone("SystemV/HST10")), SYSTEMV_MST7(TimeZone + .getTimeZone("SystemV/MST7")), SYSTEMV_MST7MDT(TimeZone.getTimeZone("SystemV/MST7MDT")), SYSTEMV_PST8(TimeZone.getTimeZone("SystemV/PST8")), SYSTEMV_PST8PDT(TimeZone.getTimeZone("SystemV/PST8PDT")), SYSTEMV_YST9(TimeZone + .getTimeZone("SystemV/YST9")), SYSTEMV_YST9YDT(TimeZone.getTimeZone("SystemV/YST9YDT")), TURKEY(TimeZone.getTimeZone("Turkey")), UCT(TimeZone.getTimeZone("UCT")), UNIVERSAL(TimeZone.getTimeZone("Universal")), US_ALASKA(TimeZone + .getTimeZone("US/Alaska")), US_ALEUTIAN(TimeZone.getTimeZone("US/Aleutian")), US_ARIZONA(TimeZone.getTimeZone("US/Arizona")), US_CENTRAL(TimeZone.getTimeZone("US/Central")), US_EAST_INDIANA(TimeZone + .getTimeZone("US/East-Indiana")), US_EASTERN(TimeZone.getTimeZone("US/Eastern")), US_HAWAII(TimeZone.getTimeZone("US/Hawaii")), US_INDIANA_STARKE(TimeZone.getTimeZone("US/Indiana-Starke")), US_MICHIGAN(TimeZone + .getTimeZone("US/Michigan")), US_MOUNTAIN(TimeZone.getTimeZone("US/Mountain")), US_PACIFIC(TimeZone.getTimeZone("US/Pacific")), US_PACIFIC_NEW(TimeZone.getTimeZone("US/Pacific-New")), US_SAMOA(TimeZone.getTimeZone("US/Samoa")), UTC( + TimeZone.getTimeZone("UTC")), VST(TimeZone.getTimeZone("VST")), W_SU(TimeZone.getTimeZone("W-SU")), WET(TimeZone.getTimeZone("WET")), ZULU(TimeZone.getTimeZone("Zulu")); + + private TimeZone tz; + + TimeZones(final TimeZone tz) { + this.tz = tz; + } + + public final TimeZone getTimeZone() { + return tz; + } + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/EditText.java b/widgets/src/main/java/mohammadaminha/com/widgets/EditText.java new file mode 100644 index 0000000..46ce436 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/EditText.java @@ -0,0 +1,36 @@ +package mohammadaminha.com.widgets; + +import android.content.Context; +import android.support.v7.widget.AppCompatEditText; +import android.util.AttributeSet; + +/** + * Created by aj on 1/30/2018. + */ + +public class EditText extends AppCompatEditText { + + + + public EditText(Context context) { + super(context); + setTf(context); + } + + public EditText(Context context, AttributeSet attrs) { + super(context, attrs); + setTf(context); + } + + public EditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setTf(context); + } + + private void setTf(Context context) { + + setTypeface(Util.getTypeFace()); + } + + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/ExpendableLayout/ExpandableLayout.java b/widgets/src/main/java/mohammadaminha/com/widgets/ExpendableLayout/ExpandableLayout.java new file mode 100644 index 0000000..3627503 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/ExpendableLayout/ExpandableLayout.java @@ -0,0 +1,328 @@ +package mohammadaminha.com.widgets.ExpendableLayout; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import mohammadaminha.com.widgets.ExpendableLayout.util.FastOutSlowInInterpolator; +import mohammadaminha.com.widgets.R; + +import static mohammadaminha.com.widgets.ExpendableLayout.ExpandableLayout.State.COLLAPSED; +import static mohammadaminha.com.widgets.ExpendableLayout.ExpandableLayout.State.COLLAPSING; +import static mohammadaminha.com.widgets.ExpendableLayout.ExpandableLayout.State.EXPANDED; +import static mohammadaminha.com.widgets.ExpendableLayout.ExpandableLayout.State.EXPANDING; + + +public class ExpandableLayout extends FrameLayout { + public interface State { + int COLLAPSED = 0; + int COLLAPSING = 1; + int EXPANDING = 2; + int EXPANDED = 3; + } + + public static final String KEY_SUPER_STATE = "super_state"; + public static final String KEY_EXPANSION = "expansion"; + + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + private static final int DEFAULT_DURATION = 300; + + private int duration = DEFAULT_DURATION; + private float parallax; + private float expansion; + private int orientation; + private int state; + + private Interpolator interpolator = new FastOutSlowInInterpolator(); + private ValueAnimator animator; + + private OnExpansionUpdateListener listener; + + public ExpandableLayout(Context context) { + this(context, null); + } + + public ExpandableLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + if (attrs != null) { + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ExpandableLayout); + duration = a.getInt(R.styleable.ExpandableLayout_el_duration, DEFAULT_DURATION); + expansion = a.getBoolean(R.styleable.ExpandableLayout_el_expanded, false) ? 1 : 0; + orientation = a.getInt(R.styleable.ExpandableLayout_android_orientation, VERTICAL); + parallax = a.getFloat(R.styleable.ExpandableLayout_el_parallax, 1); + a.recycle(); + + state = expansion == 0 ? COLLAPSED : EXPANDED; + setParallax(parallax); + } + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + Bundle bundle = new Bundle(); + + expansion = isExpanded() ? 1 : 0; + + bundle.putFloat(KEY_EXPANSION, expansion); + bundle.putParcelable(KEY_SUPER_STATE, superState); + + return bundle; + } + + @Override + protected void onRestoreInstanceState(Parcelable parcelable) { + Bundle bundle = (Bundle) parcelable; + expansion = bundle.getFloat(KEY_EXPANSION); + state = expansion == 1 ? EXPANDED : COLLAPSED; + Parcelable superState = bundle.getParcelable(KEY_SUPER_STATE); + + super.onRestoreInstanceState(superState); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + + int size = orientation == LinearLayout.HORIZONTAL ? width : height; + + setVisibility(expansion == 0 && size == 0 ? GONE : VISIBLE); + + int expansionDelta = size - Math.round(size * expansion); + if (parallax > 0) { + float parallaxDelta = expansionDelta * parallax; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (orientation == HORIZONTAL) { + int direction = -1; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1 && getLayoutDirection() == LAYOUT_DIRECTION_RTL) { + direction = 1; + } + child.setTranslationX(direction * parallaxDelta); + } else { + child.setTranslationY(-parallaxDelta); + } + } + } + + if (orientation == HORIZONTAL) { + setMeasuredDimension(width - expansionDelta, height); + } else { + setMeasuredDimension(width, height - expansionDelta); + } + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + if (animator != null) { + animator.cancel(); + } + super.onConfigurationChanged(newConfig); + } + + /** + * Get expansion state + * + * @return one of {@link State} + */ + public int getState() { + return state; + } + + public boolean isExpanded() { + return state == EXPANDING || state == EXPANDED; + } + + public void toggle() { + toggle(true); + } + + public void toggle(boolean animate) { + if (isExpanded()) { + collapse(animate); + } else { + expand(animate); + } + } + + public void expand() { + expand(true); + } + + public void expand(boolean animate) { + setExpanded(true, animate); + } + + public void collapse() { + collapse(true); + } + + public void collapse(boolean animate) { + setExpanded(false, animate); + } + + /** + * Convenience method - same as calling setExpanded(expanded, true) + */ + public void setExpanded(boolean expand) { + setExpanded(expand, true); + } + + public void setExpanded(boolean expand, boolean animate) { + if (expand == isExpanded()) { + return; + } + + int targetExpansion = expand ? 1 : 0; + if (animate) { + animateSize(targetExpansion); + } else { + setExpansion(targetExpansion); + } + } + + public int getDuration() { + return duration; + } + + public void setInterpolator(Interpolator interpolator) { + this.interpolator = interpolator; + } + + public void setDuration(int duration) { + this.duration = duration; + } + + public float getExpansion() { + return expansion; + } + + public void setExpansion(float expansion) { + if (this.expansion == expansion) { + return; + } + + // Infer state from previous value + float delta = expansion - this.expansion; + if (expansion == 0) { + state = COLLAPSED; + } else if (expansion == 1) { + state = EXPANDED; + } else if (delta < 0) { + state = COLLAPSING; + } else if (delta > 0) { + state = EXPANDING; + } + + setVisibility(state == COLLAPSED ? GONE : VISIBLE); + this.expansion = expansion; + requestLayout(); + + if (listener != null) { + listener.onExpansionUpdate(expansion, state); + } + } + + public float getParallax() { + return parallax; + } + + public void setParallax(float parallax) { + // Make sure parallax is between 0 and 1 + parallax = Math.min(1, Math.max(0, parallax)); + this.parallax = parallax; + } + + public int getOrientation() { + return orientation; + } + + public void setOrientation(int orientation) { + if (orientation < 0 || orientation > 1) { + throw new IllegalArgumentException("Orientation must be either 0 (horizontal) or 1 (vertical)"); + } + this.orientation = orientation; + } + + public void setOnExpansionUpdateListener(OnExpansionUpdateListener listener) { + this.listener = listener; + } + + private void animateSize(int targetExpansion) { + if (animator != null) { + animator.cancel(); + animator = null; + } + + animator = ValueAnimator.ofFloat(expansion, targetExpansion); + animator.setInterpolator(interpolator); + animator.setDuration(duration); + + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + setExpansion((float) valueAnimator.getAnimatedValue()); + } + }); + + animator.addListener(new ExpansionListener(targetExpansion)); + + animator.start(); + } + + public interface OnExpansionUpdateListener { + /** + * Callback for expansion updates + * + * @param expansionFraction Value between 0 (collapsed) and 1 (expanded) representing the the expansion progress + * @param state One of {@link State} repesenting the current expansion state + */ + void onExpansionUpdate(float expansionFraction, int state); + } + + private class ExpansionListener implements Animator.AnimatorListener { + private int targetExpansion; + private boolean canceled; + + public ExpansionListener(int targetExpansion) { + this.targetExpansion = targetExpansion; + } + + @Override + public void onAnimationStart(Animator animation) { + state = targetExpansion == 0 ? COLLAPSING : EXPANDING; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!canceled) { + state = targetExpansion == 0 ? COLLAPSED : EXPANDED; + setExpansion(targetExpansion); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + canceled = true; + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/ExpendableLayout/util/FastOutSlowInInterpolator.java b/widgets/src/main/java/mohammadaminha/com/widgets/ExpendableLayout/util/FastOutSlowInInterpolator.java new file mode 100644 index 0000000..5d304f5 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/ExpendableLayout/util/FastOutSlowInInterpolator.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2015 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. + */ + +package mohammadaminha.com.widgets.ExpendableLayout.util; + +/** + * Interpolator corresponding to {@link android.R.interpolator#fast_out_slow_in}. + * + * Uses a lookup table for the Bezier curve from (0,0) to (1,1) with control points: + * P0 (0, 0) + * P1 (0.4, 0) + * P2 (0.2, 1.0) + * P3 (1.0, 1.0) + */ +public class FastOutSlowInInterpolator extends LookupTableInterpolator { + + /** + * Lookup table values sampled with x at regular intervals between 0 and 1 for a total of + * 201 points. + */ + private static final float[] VALUES = new float[] { + 0.0000f, 0.0001f, 0.0002f, 0.0005f, 0.0009f, 0.0014f, 0.0020f, + 0.0027f, 0.0036f, 0.0046f, 0.0058f, 0.0071f, 0.0085f, 0.0101f, + 0.0118f, 0.0137f, 0.0158f, 0.0180f, 0.0205f, 0.0231f, 0.0259f, + 0.0289f, 0.0321f, 0.0355f, 0.0391f, 0.0430f, 0.0471f, 0.0514f, + 0.0560f, 0.0608f, 0.0660f, 0.0714f, 0.0771f, 0.0830f, 0.0893f, + 0.0959f, 0.1029f, 0.1101f, 0.1177f, 0.1257f, 0.1339f, 0.1426f, + 0.1516f, 0.1610f, 0.1707f, 0.1808f, 0.1913f, 0.2021f, 0.2133f, + 0.2248f, 0.2366f, 0.2487f, 0.2611f, 0.2738f, 0.2867f, 0.2998f, + 0.3131f, 0.3265f, 0.3400f, 0.3536f, 0.3673f, 0.3810f, 0.3946f, + 0.4082f, 0.4217f, 0.4352f, 0.4485f, 0.4616f, 0.4746f, 0.4874f, + 0.5000f, 0.5124f, 0.5246f, 0.5365f, 0.5482f, 0.5597f, 0.5710f, + 0.5820f, 0.5928f, 0.6033f, 0.6136f, 0.6237f, 0.6335f, 0.6431f, + 0.6525f, 0.6616f, 0.6706f, 0.6793f, 0.6878f, 0.6961f, 0.7043f, + 0.7122f, 0.7199f, 0.7275f, 0.7349f, 0.7421f, 0.7491f, 0.7559f, + 0.7626f, 0.7692f, 0.7756f, 0.7818f, 0.7879f, 0.7938f, 0.7996f, + 0.8053f, 0.8108f, 0.8162f, 0.8215f, 0.8266f, 0.8317f, 0.8366f, + 0.8414f, 0.8461f, 0.8507f, 0.8551f, 0.8595f, 0.8638f, 0.8679f, + 0.8720f, 0.8760f, 0.8798f, 0.8836f, 0.8873f, 0.8909f, 0.8945f, + 0.8979f, 0.9013f, 0.9046f, 0.9078f, 0.9109f, 0.9139f, 0.9169f, + 0.9198f, 0.9227f, 0.9254f, 0.9281f, 0.9307f, 0.9333f, 0.9358f, + 0.9382f, 0.9406f, 0.9429f, 0.9452f, 0.9474f, 0.9495f, 0.9516f, + 0.9536f, 0.9556f, 0.9575f, 0.9594f, 0.9612f, 0.9629f, 0.9646f, + 0.9663f, 0.9679f, 0.9695f, 0.9710f, 0.9725f, 0.9739f, 0.9753f, + 0.9766f, 0.9779f, 0.9791f, 0.9803f, 0.9815f, 0.9826f, 0.9837f, + 0.9848f, 0.9858f, 0.9867f, 0.9877f, 0.9885f, 0.9894f, 0.9902f, + 0.9910f, 0.9917f, 0.9924f, 0.9931f, 0.9937f, 0.9944f, 0.9949f, + 0.9955f, 0.9960f, 0.9964f, 0.9969f, 0.9973f, 0.9977f, 0.9980f, + 0.9984f, 0.9986f, 0.9989f, 0.9991f, 0.9993f, 0.9995f, 0.9997f, + 0.9998f, 0.9999f, 0.9999f, 1.0000f, 1.0000f + }; + + public FastOutSlowInInterpolator() { + super(VALUES); + } + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/ExpendableLayout/util/LookupTableInterpolator.java b/widgets/src/main/java/mohammadaminha/com/widgets/ExpendableLayout/util/LookupTableInterpolator.java new file mode 100644 index 0000000..593a539 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/ExpendableLayout/util/LookupTableInterpolator.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015 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. + */ + +package mohammadaminha.com.widgets.ExpendableLayout.util; + +import android.view.animation.Interpolator; + +/** + * An {@link Interpolator} that uses a lookup table to compute an interpolation based on a + * given input. + */ +abstract class LookupTableInterpolator implements Interpolator { + + private final float[] mValues; + private final float mStepSize; + + public LookupTableInterpolator(float[] values) { + mValues = values; + mStepSize = 1f / (mValues.length - 1); + } + + @Override + public float getInterpolation(float input) { + if (input >= 1.0f) { + return 1.0f; + } + if (input <= 0f) { + return 0f; + } + + // Calculate index - We use min with length - 2 to avoid IndexOutOfBoundsException when + // we lerp (linearly interpolate) in the return statement + int position = Math.min((int) (input * (mValues.length - 1)), mValues.length - 2); + + // Calculate values to account for small offsets as the lookup table has discrete values + float quantized = position * mStepSize; + float diff = input - quantized; + float weight = diff / mStepSize; + + // Linearly interpolate between the table values + return mValues[position] + weight * (mValues[position + 1] - mValues[position]); + } + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/GridSpacingItemDecoration.java b/widgets/src/main/java/mohammadaminha/com/widgets/GridSpacingItemDecoration.java new file mode 100644 index 0000000..93305fa --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/GridSpacingItemDecoration.java @@ -0,0 +1,50 @@ +package mohammadaminha.com.widgets; +import android.graphics.Rect; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration { + + private int spanCount; + private int spacing; + private boolean includeEdge; + private int headerNum; + + public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge, int headerNum) { + this.spanCount = spanCount; + this.spacing = spacing; + this.includeEdge = includeEdge; + this.headerNum = headerNum; + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + int position = parent.getChildAdapterPosition(view) - headerNum; // item position + + if (position >= 0) { + int column = position % spanCount; // item column + + if (includeEdge) { + outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing) + outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing) + + if (position < spanCount) { // top edge + outRect.top = spacing; + } + outRect.bottom = spacing; // item bottom + } else { + outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing) + outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing) + if (position >= spanCount) { + outRect.top = spacing; // item top + } + } + } else { + outRect.left = 0; + outRect.right = 0; + outRect.top = 0; + outRect.bottom = 0; + } + } +} + diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/ImageViewTouch.java b/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/ImageViewTouch.java new file mode 100644 index 0000000..73d870f --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/ImageViewTouch.java @@ -0,0 +1,404 @@ +package mohammadaminha.com.widgets.ImageViewZoom; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.view.GestureDetector; +import android.view.GestureDetector.OnGestureListener; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.ScaleGestureDetector.OnScaleGestureListener; +import android.view.ViewConfiguration; + +public class ImageViewTouch extends ImageViewTouchBase { + private static final float SCROLL_DELTA_THRESHOLD = 1.0f; + /** + * minimum time between a scale event and a valid fling event + */ + private static final long MIN_FLING_DELTA_TIME = 150; + private float mScaleFactor; + private ScaleGestureDetector mScaleDetector; + private GestureDetector mGestureDetector; + private int mTouchSlop; + private int mDoubleTapDirection; + private OnGestureListener mGestureListener; + private OnScaleGestureListener mScaleListener; + private boolean mDoubleTapEnabled = true; + private boolean mScaleEnabled = true; + private boolean mScrollEnabled = true; + private OnImageViewTouchDoubleTapListener mDoubleTapListener; + private OnImageViewTouchSingleTapListener mSingleTapListener; + + public ImageViewTouch(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ImageViewTouch(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init(Context context, AttributeSet attrs, int defStyle) { + super.init(context, attrs, defStyle); + mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + mGestureListener = getGestureListener(); + mScaleListener = getScaleListener(); + + mScaleDetector = new ScaleGestureDetector(getContext(), mScaleListener); + mGestureDetector = new GestureDetector(getContext(), mGestureListener, null, true); + mDoubleTapDirection = 1; + setQuickScaleEnabled(false); + } + + @TargetApi(19) + private void setQuickScaleEnabled(boolean value) { + if (Build.VERSION.SDK_INT >= 19) { + mScaleDetector.setQuickScaleEnabled(value); + } + } + + @TargetApi(19) + @SuppressWarnings("unused") + public boolean getQuickScaleEnabled() { + + + return Build.VERSION.SDK_INT >= 19 && mScaleDetector.isQuickScaleEnabled(); + + } + + @SuppressWarnings("unused") + public float getScaleFactor() { + return mScaleFactor; + } + + public void setDoubleTapListener(OnImageViewTouchDoubleTapListener listener) { + mDoubleTapListener = listener; + } + + public void setSingleTapListener(OnImageViewTouchSingleTapListener listener) { + mSingleTapListener = listener; + } + + public void setDoubleTapEnabled(boolean value) { + mDoubleTapEnabled = value; + } + + public void setScaleEnabled(boolean value) { + mScaleEnabled = value; + } + + public void setScrollEnabled(boolean value) { + mScrollEnabled = value; + } + + public boolean getDoubleTapEnabled() { + return mDoubleTapEnabled; + } + + private OnGestureListener getGestureListener() { + return new GestureListener(); + } + + private OnScaleGestureListener getScaleListener() { + return new ScaleListener(); + } + + @Override + protected void onLayoutChanged(final int left, final int top, final int right, final int bottom) { + super.onLayoutChanged(left, top, right, bottom); + Log.v(TAG, "min: " + getMinScale() + ", max: " + getMaxScale() + ", result: " + (getMaxScale() - getMinScale()) / 2f); + mScaleFactor = ((getMaxScale() - getMinScale()) / 2f) + 0.5f; + } + + private long mPointerUpTime; + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (getBitmapChanged()) { + return false; + } + + final int action = event.getActionMasked(); + + if (action == MotionEvent.ACTION_POINTER_UP) { + mPointerUpTime = event.getEventTime(); + } + + mScaleDetector.onTouchEvent(event); + + if (!mScaleDetector.isInProgress()) { + mGestureDetector.onTouchEvent(event); + } + + switch (action) { + case MotionEvent.ACTION_UP: + return onUp(event); + default: + break; + } + return true; + } + + @Override + protected void onZoomAnimationCompleted(float scale) { + + if (DEBUG) { + Log.d(TAG, "onZoomAnimationCompleted. scale: " + scale + ", minZoom: " + getMinScale()); + } + + if (scale < getMinScale()) { + zoomTo(getMinScale(), 50); + } + } + + private float onDoubleTapPost(float scale, final float maxZoom, final float minScale) { + if ((scale + mScaleFactor) <= maxZoom) { + return scale + mScaleFactor; + } else { + return minScale; + } + } + + private boolean onSingleTapConfirmed(MotionEvent e) { + return true; + } + + private boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + if (canScroll()) { + return false; + } + mUserScaled = true; + scrollBy(-distanceX, -distanceY); + invalidate(); + return true; + } + + private boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (canScroll()) { + return false; + } + + if (DEBUG) { + Log.i(TAG, "onFling"); + } + + if (Math.abs(velocityX) > (mMinFlingVelocity * 4) || Math.abs(velocityY) > (mMinFlingVelocity * 4)) { + if (DEBUG) { + Log.v(TAG, "velocity: " + velocityY); + Log.v(TAG, "diff: " + (e2.getY() - e1.getY())); + } + + final float scale = Math.min(Math.max(2f, getScale() / 2), 3.f); + + float scaledDistanceX = ((velocityX) / mMaxFlingVelocity) * (getWidth() * scale); + float scaledDistanceY = ((velocityY) / mMaxFlingVelocity) * (getHeight() * scale); + + if (DEBUG) { + Log.v(TAG, "scale: " + getScale() + ", scale_final: " + scale); + Log.v(TAG, "scaledDistanceX: " + scaledDistanceX); + Log.v(TAG, "scaledDistanceY: " + scaledDistanceY); + } + + mUserScaled = true; + + double total = Math.sqrt(Math.pow(scaledDistanceX, 2) + Math.pow(scaledDistanceY, 2)); + + scrollBy(scaledDistanceX, scaledDistanceY, (long) Math.min(Math.max(300, total / 5), 800)); + + postInvalidate(); + return true; + } + return false; + } + + private boolean onDown(MotionEvent e) { + return !getBitmapChanged(); + } + + private boolean onUp(MotionEvent e) { + if (getBitmapChanged()) { + return false; + } + if (getScale() < getMinScale()) { + zoomTo(getMinScale(), 50); + } + return true; + } + + private boolean onSingleTapUp(MotionEvent e) { + return !getBitmapChanged(); + } + + private boolean canScroll() { + if (getScale() > 1) { + return false; + } + RectF bitmapRect = getBitmapRect(); + return mViewPort.contains(bitmapRect); + } + + /** + * Determines whether this ImageViewTouch can be scrolled. + * + * @param direction - positive direction value means scroll from right to left, + * negative value means scroll from left to right + * @return true if there is some more place to scroll, false - otherwise. + */ + @SuppressWarnings("unused") + public boolean canScroll(int direction) { + RectF bitmapRect = getBitmapRect(); + updateRect(bitmapRect, mScrollPoint); + Rect imageViewRect = new Rect(); + getGlobalVisibleRect(imageViewRect); + + if (null == bitmapRect) { + return false; + } + + if (bitmapRect.right >= imageViewRect.right) { + if (direction < 0) { + return Math.abs(bitmapRect.right - imageViewRect.right) > SCROLL_DELTA_THRESHOLD; + } + } + + double bitmapScrollRectDelta = Math.abs(bitmapRect.left - mScrollPoint.x); + return bitmapScrollRectDelta > SCROLL_DELTA_THRESHOLD; + } + + public class GestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + + if (null != mSingleTapListener) { + mSingleTapListener.onSingleTapConfirmed(); + } + + return ImageViewTouch.this.onSingleTapConfirmed(e); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + if (DEBUG) { + Log.i(TAG, "onDoubleTap. double tap enabled? " + mDoubleTapEnabled); + } + if (mDoubleTapEnabled) { + if (Build.VERSION.SDK_INT >= 19) { + if (mScaleDetector.isQuickScaleEnabled()) { + return true; + } + } + + mUserScaled = true; + + float scale = getScale(); + float targetScale; + targetScale = onDoubleTapPost(scale, getMaxScale(), getMinScale()); + targetScale = Math.min(getMaxScale(), Math.max(targetScale, getMinScale())); + zoomTo(targetScale, e.getX(), e.getY(), mDefaultAnimationDuration); + + } + + if (null != mDoubleTapListener) { + mDoubleTapListener.onDoubleTap(); + } + + return super.onDoubleTap(e); + } + + @Override + public void onLongPress(MotionEvent e) { + if (isLongClickable()) { + if (!mScaleDetector.isInProgress()) { + setPressed(true); + performLongClick(); + } + } + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + return mScrollEnabled && !(e1 == null || e2 == null) && !(e1.getPointerCount() > 1 || e2.getPointerCount() > 1) && !mScaleDetector.isInProgress() && ImageViewTouch.this.onScroll(e1, e2, distanceX, distanceY); + + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (!mScrollEnabled) { + return false; + } + if (e1 == null || e2 == null) { + return false; + } + if (e1.getPointerCount() > 1 || e2.getPointerCount() > 1) { + return false; + } + if (mScaleDetector.isInProgress()) { + return false; + } + + final long delta = (SystemClock.uptimeMillis() - mPointerUpTime); + + // prevent fling happening just + // after a quick pinch to zoom + return delta > MIN_FLING_DELTA_TIME && ImageViewTouch.this.onFling(e1, e2, velocityX, velocityY); + + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + return ImageViewTouch.this.onSingleTapUp(e); + } + + @Override + public boolean onDown(MotionEvent e) { + if (DEBUG) { + Log.i(TAG, "onDown"); + } + stopAllAnimations(); + + return ImageViewTouch.this.onDown(e); + } + } + + public class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + boolean mScaled = false; + + @Override + public boolean onScale(ScaleGestureDetector detector) { + float span = detector.getCurrentSpan() - detector.getPreviousSpan(); + float targetScale = getScale() * detector.getScaleFactor(); + + if (mScaleEnabled) { + if (mScaled && span != 0) { + mUserScaled = true; + targetScale = Math.min(getMaxScale(), Math.max(targetScale, getMinScale() - MIN_SCALE_DIFF)); + zoomTo(targetScale, detector.getFocusX(), detector.getFocusY()); + mDoubleTapDirection = 1; + invalidate(); + return true; + } + + // This is to prevent a glitch the first time + // image is scaled. + if (!mScaled) { + mScaled = true; + } + } + return true; + } + + } + + public interface OnImageViewTouchDoubleTapListener { + void onDoubleTap(); + } + + public interface OnImageViewTouchSingleTapListener { + void onSingleTapConfirmed(); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/ImageViewTouchBase.java b/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/ImageViewTouchBase.java new file mode 100644 index 0000000..0aaeb30 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/ImageViewTouchBase.java @@ -0,0 +1,957 @@ +package mohammadaminha.com.widgets.ImageViewZoom; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ViewConfiguration; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; + +import mohammadaminha.com.widgets.BuildConfig; +import mohammadaminha.com.widgets.ImageViewZoom.graphics.FastBitmapDrawable; +import mohammadaminha.com.widgets.ImageViewZoom.utils.IDisposable; + + +/** + * Base View to manage image zoom/scrool/pinch operations + * + * @author alessandro + */ +public abstract class ImageViewTouchBase extends android.support.v7.widget.AppCompatImageView implements IDisposable { + public static final String VERSION = BuildConfig.VERSION_NAME; + static final float MIN_SCALE_DIFF = 0.1f; + + public interface OnDrawableChangeListener { + void onDrawableChanged(Drawable drawable); + } + + public interface OnLayoutChangeListener { + /** + * Callback invoked when the layout bounds changed + */ + void onLayoutChanged(boolean changed, int left, int top, int right, int bottom); + } + + /** + * Use this to change the {@link ImageViewTouchBase#setDisplayType(DisplayType)} of + * this View + * + * @author alessandro + */ + public enum DisplayType { + /** + * Image is not scaled by default + */ + NONE, + /** + * Image will be always presented using this view's bounds + */ + FIT_TO_SCREEN, + /** + * Image will be scaled only if bigger than the bounds of this view + */ + FIT_IF_BIGGER + } + + static final String TAG = "ImageViewTouchBase"; + @SuppressWarnings("checkstyle:staticvariablename") + static final boolean DEBUG = false; + private static final float ZOOM_INVALID = -1f; + private final Matrix mBaseMatrix = new Matrix(); + private Matrix mSuppMatrix = new Matrix(); + private Matrix mNextMatrix; + private Runnable mLayoutRunnable = null; + boolean mUserScaled = false; + private float mMaxZoom = ZOOM_INVALID; + private float mMinZoom = ZOOM_INVALID; + // true when min and max zoom are explicitly defined + private boolean mMaxZoomDefined; + private boolean mMinZoomDefined; + private final Matrix mDisplayMatrix = new Matrix(); + private final float[] mMatrixValues = new float[9]; + private DisplayType mScaleType = DisplayType.FIT_IF_BIGGER; + private boolean mScaleTypeChanged; + private boolean mBitmapChanged; + int mDefaultAnimationDuration; + int mMinFlingVelocity; + int mMaxFlingVelocity; + private final PointF mCenter = new PointF(); + private final RectF mBitmapRect = new RectF(); + private final RectF mBitmapRectTmp = new RectF(); + private final RectF mCenterRect = new RectF(); + final PointF mScrollPoint = new PointF(); + final RectF mViewPort = new RectF(); + private final RectF mViewPortOld = new RectF(); + private Animator mCurrentAnimation; + private OnDrawableChangeListener mDrawableChangeListener; + private OnLayoutChangeListener mOnLayoutChangeListener; + + public ImageViewTouchBase(Context context) { + this(context, null); + } + + public ImageViewTouchBase(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ImageViewTouchBase(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + boolean getBitmapChanged() { + return mBitmapChanged; + } + + public void setOnDrawableChangedListener(OnDrawableChangeListener listener) { + mDrawableChangeListener = listener; + } + + public void setOnLayoutChangeListener(OnLayoutChangeListener listener) { + mOnLayoutChangeListener = listener; + } + + void init(Context context, AttributeSet attrs, int defStyle) { + ViewConfiguration configuration = ViewConfiguration.get(context); + mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaxFlingVelocity = configuration.getScaledMaximumFlingVelocity(); + mDefaultAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime); + setScaleType(ScaleType.MATRIX); + } + + /** + * Clear the current drawable + */ + private void clear() { + setImageBitmap(null); + } + + public void setDisplayType(DisplayType type) { + if (type != mScaleType) { + if (DEBUG) { + Log.i(TAG, "setDisplayType: " + type); + } + mUserScaled = false; + mScaleType = type; + mScaleTypeChanged = true; + requestLayout(); + } + } + + private DisplayType getDisplayType() { + return mScaleType; + } + + protected void setMinScale(float value) { + if (DEBUG) { + Log.d(TAG, "setMinZoom: " + value); + } + + mMinZoom = value; + } + + protected void setMaxScale(float value) { + if (DEBUG) { + Log.d(TAG, "setMaxZoom: " + value); + } + mMaxZoom = value; + } + + private void onViewPortChanged(float left, float top, float right, float bottom) { + mViewPort.set(left, top, right, bottom); + mCenter.x = mViewPort.centerX(); + mCenter.y = mViewPort.centerY(); + } + + @SuppressWarnings("checkstyle:cyclomaticcomplexity") + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (DEBUG) { + Log.e(TAG, "onLayout: " + changed + ", bitmapChanged: " + mBitmapChanged + ", scaleChanged: " + mScaleTypeChanged); + } + + float deltaX = 0; + float deltaY = 0; + + if (changed) { + mViewPortOld.set(mViewPort); + onViewPortChanged(left, top, right, bottom); + + deltaX = mViewPort.width() - mViewPortOld.width(); + deltaY = mViewPort.height() - mViewPortOld.height(); + } + + super.onLayout(changed, left, top, right, bottom); + + Runnable r = mLayoutRunnable; + + if (r != null) { + mLayoutRunnable = null; + r.run(); + } + + final Drawable drawable = getDrawable(); + + if (drawable != null) { + + if (changed || mScaleTypeChanged || mBitmapChanged) { + + if (mBitmapChanged) { + mUserScaled = false; + mBaseMatrix.reset(); + if (!mMinZoomDefined) { + mMinZoom = ZOOM_INVALID; + } + if (!mMaxZoomDefined) { + mMaxZoom = ZOOM_INVALID; + } + } + + float scale = 1; + + // retrieve the old values + float oldDefaultScale = getDefaultScale(getDisplayType()); + float oldMatrixScale = getScale(mBaseMatrix); + float oldScale = getScale(); + float oldMinScale = Math.min(1f, 1f / oldMatrixScale); + + getProperBaseMatrix(drawable, mBaseMatrix, mViewPort); + + float newMatrixScale = getScale(mBaseMatrix); + + if (DEBUG) { + Log.d(TAG, "old matrix scale: " + oldMatrixScale); + Log.d(TAG, "new matrix scale: " + newMatrixScale); + Log.d(TAG, "old min scale: " + oldMinScale); + Log.d(TAG, "old scale: " + oldScale); + } + + // 1. bitmap changed or scaletype changed + if (mBitmapChanged || mScaleTypeChanged) { + + if (DEBUG) { + Log.d(TAG, "display type: " + getDisplayType()); + Log.d(TAG, "newMatrix: " + mNextMatrix); + } + + if (mNextMatrix != null) { + mSuppMatrix.set(mNextMatrix); + mNextMatrix = null; + scale = getScale(); + } else { + mSuppMatrix.reset(); + scale = getDefaultScale(getDisplayType()); + } + + setImageMatrix(getImageViewMatrix()); + + if (scale != getScale()) { + if (DEBUG) { + Log.v(TAG, "scale != getScale: " + scale + " != " + getScale()); + } + zoomTo(scale); + } + + } else if (changed) { + + // 2. layout size changed + + if (!mMinZoomDefined) { + mMinZoom = ZOOM_INVALID; + } + if (!mMaxZoomDefined) { + mMaxZoom = ZOOM_INVALID; + } + + setImageMatrix(getImageViewMatrix()); + postTranslate(-deltaX, -deltaY); + + if (!mUserScaled) { + scale = getDefaultScale(getDisplayType()); + if (DEBUG) { + Log.v(TAG, "!userScaled. scale=" + scale); + } + zoomTo(scale); + } else { + if (Math.abs(oldScale - oldMinScale) > MIN_SCALE_DIFF) { + scale = (oldMatrixScale / newMatrixScale) * oldScale; + } + if (DEBUG) { + Log.v(TAG, "userScaled. scale=" + scale); + } + zoomTo(scale); + } + + if (DEBUG) { + Log.d(TAG, "old min scale: " + oldDefaultScale); + Log.d(TAG, "old scale: " + oldScale); + Log.d(TAG, "new scale: " + scale); + } + } + + if (scale > getMaxScale() || scale < getMinScale()) { + // if current scale if outside the min/max bounds + // then restore the correct scale + zoomTo(scale); + } + + center(true, true); + + if (mBitmapChanged) { + onDrawableChanged(drawable); + } + if (changed || mBitmapChanged || mScaleTypeChanged) { + onLayoutChanged(left, top, right, bottom); + } + + if (mScaleTypeChanged) { + mScaleTypeChanged = false; + } + if (mBitmapChanged) { + mBitmapChanged = false; + } + + if (DEBUG) { + Log.d(TAG, "scale: " + getScale() + ", minScale: " + getMinScale() + ", maxScale: " + getMaxScale()); + } + } + } else { + // drawable is null + if (mBitmapChanged) { + onDrawableChanged(drawable); + } + if (changed || mBitmapChanged || mScaleTypeChanged) { + onLayoutChanged(left, top, right, bottom); + } + + if (mBitmapChanged) { + mBitmapChanged = false; + } + if (mScaleTypeChanged) { + mScaleTypeChanged = false; + } + } + } + + @Override + protected void onConfigurationChanged(final Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + if (DEBUG) { + Log.i( + TAG, + "onConfigurationChanged. scale: " + getScale() + ", minScale: " + getMinScale() + ", mUserScaled: " + mUserScaled + ); + } + + if (mUserScaled) { + mUserScaled = Math.abs(getScale() - getMinScale()) > MIN_SCALE_DIFF; + } + + if (DEBUG) { + Log.v(TAG, "mUserScaled: " + mUserScaled); + } + } + + /** + * Restore the original display + */ + public void resetDisplay() { + mBitmapChanged = true; + requestLayout(); + } + + public void resetMatrix() { + if (DEBUG) { + Log.i(TAG, "resetMatrix"); + } + mSuppMatrix = new Matrix(); + + float scale = getDefaultScale(getDisplayType()); + setImageMatrix(getImageViewMatrix()); + + if (DEBUG) { + Log.d(TAG, "default scale: " + scale + ", scale: " + getScale()); + } + + if (scale != getScale()) { + zoomTo(scale); + } + + postInvalidate(); + } + + private float getDefaultScale(DisplayType type) { + if (type == DisplayType.FIT_TO_SCREEN) { + // always fit to screen + return 1f; + } else if (type == DisplayType.FIT_IF_BIGGER) { + // normal scale if smaller, fit to screen otherwise + return Math.min(1f, 1f / getScale(mBaseMatrix)); + } else { + // no scale + return 1f / getScale(mBaseMatrix); + } + } + + @Override + public void setImageResource(int resId) { + setImageDrawable(getContext().getResources().getDrawable(resId)); + } + + /** + * {@inheritDoc} Set the new image to display and reset the internal matrix. + * + * @param bitmap the {@link Bitmap} to display + * @see {@link ImageView#setImageBitmap(Bitmap)} + */ + @Override + public void setImageBitmap(final Bitmap bitmap) { + setImageBitmap(bitmap, null, ZOOM_INVALID, ZOOM_INVALID); + } + + private void setImageBitmap(final Bitmap bitmap, Matrix matrix, float minZoom, float maxZoom) { + if (bitmap != null) { + setImageDrawable(new FastBitmapDrawable(bitmap), matrix, minZoom, maxZoom); + } else { + setImageDrawable(null, matrix, minZoom, maxZoom); + } + } + + @Override + public void setImageDrawable(Drawable drawable) { + setImageDrawable(drawable, null, ZOOM_INVALID, ZOOM_INVALID); + } + + /** + * Note: if the scaleType is FitToScreen then min_zoom must be <= 1 and max_zoom must be >= 1 + * + * @param drawable the new drawable + * @param initialMatrix the optional initial display matrix + * @param minZoom the optional minimum scale, pass {@link #ZOOM_INVALID} to use the default min_zoom + * @param maxZoom the optional maximum scale, pass {@link #ZOOM_INVALID} to use the default max_zoom + */ + private void setImageDrawable(final Drawable drawable, final Matrix initialMatrix, final float minZoom, final float maxZoom) { + final int viewWidth = getWidth(); + + if (viewWidth <= 0) { + mLayoutRunnable = new Runnable() { + @Override + public void run() { + setImageDrawable(drawable, initialMatrix, minZoom, maxZoom); + } + }; + return; + } + setImageDrawableInternal(drawable, initialMatrix, minZoom, maxZoom); + } + + private void setImageDrawableInternal(final Drawable drawable, final Matrix initialMatrix, float minZoom, float maxZoom) { + mBaseMatrix.reset(); + super.setImageDrawable(drawable); + + if (minZoom != ZOOM_INVALID && maxZoom != ZOOM_INVALID) { + minZoom = Math.min(minZoom, maxZoom); + maxZoom = Math.max(minZoom, maxZoom); + + mMinZoom = minZoom; + mMaxZoom = maxZoom; + + mMinZoomDefined = true; + mMaxZoomDefined = true; + + if (getDisplayType() == DisplayType.FIT_TO_SCREEN || getDisplayType() == DisplayType.FIT_IF_BIGGER) { + + if (mMinZoom >= 1) { + mMinZoomDefined = false; + mMinZoom = ZOOM_INVALID; + } + + if (mMaxZoom <= 1) { + mMaxZoomDefined = true; + mMaxZoom = ZOOM_INVALID; + } + } + } else { + mMinZoom = ZOOM_INVALID; + mMaxZoom = ZOOM_INVALID; + + mMinZoomDefined = false; + mMaxZoomDefined = false; + } + + if (initialMatrix != null) { + mNextMatrix = new Matrix(initialMatrix); + } + if (DEBUG) { + Log.v(TAG, "mMinZoom: " + mMinZoom + ", mMaxZoom: " + mMaxZoom); + } + + mBitmapChanged = true; + updateDrawable(drawable); + requestLayout(); + } + + private void updateDrawable(Drawable newDrawable) { + if (null != newDrawable) { + mBitmapRect.set(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight()); + } else { + mBitmapRect.setEmpty(); + } + } + + private void onDrawableChanged(final Drawable drawable) { + if (DEBUG) { + Log.i(TAG, "onDrawableChanged"); + Log.v(TAG, "scale: " + getScale() + ", minScale: " + getMinScale()); + } + fireOnDrawableChangeListener(drawable); + } + + private void fireOnLayoutChangeListener(int left, int top, int right, int bottom) { + if (null != mOnLayoutChangeListener) { + mOnLayoutChangeListener.onLayoutChanged(true, left, top, right, bottom); + } + } + + private void fireOnDrawableChangeListener(Drawable drawable) { + if (null != mDrawableChangeListener) { + mDrawableChangeListener.onDrawableChanged(drawable); + } + } + + void onLayoutChanged(int left, int top, int right, int bottom) { + if (DEBUG) { + Log.i(TAG, "onLayoutChanged"); + } + fireOnLayoutChangeListener(left, top, right, bottom); + } + + private float computeMaxZoom() { + final Drawable drawable = getDrawable(); + if (drawable == null) { + return 1f; + } + float fw = mBitmapRect.width() / mViewPort.width(); + float fh = mBitmapRect.height() / mViewPort.height(); + float scale = Math.max(fw, fh) * 4; + + if (DEBUG) { + Log.i(TAG, "computeMaxZoom: " + scale); + } + return scale; + } + + private float computeMinZoom() { + if (DEBUG) { + Log.i(TAG, "computeMinZoom"); + } + + final Drawable drawable = getDrawable(); + if (drawable == null) { + return 1f; + } + + float scale = getScale(mBaseMatrix); + + scale = Math.min(1f, 1f / scale); + if (DEBUG) { + Log.i(TAG, "computeMinZoom: " + scale); + } + + return scale; + } + + float getMaxScale() { + if (mMaxZoom == ZOOM_INVALID) { + mMaxZoom = computeMaxZoom(); + } + return mMaxZoom; + } + + float getMinScale() { + if (DEBUG) { + Log.i(TAG, "getMinScale, mMinZoom: " + mMinZoom); + } + + if (mMinZoom == ZOOM_INVALID) { + mMinZoom = computeMinZoom(); + } + + if (DEBUG) { + Log.v(TAG, "mMinZoom: " + mMinZoom); + } + + return mMinZoom; + } + + private Matrix getImageViewMatrix() { + return getImageViewMatrix(mSuppMatrix); + } + + private Matrix getImageViewMatrix(Matrix supportMatrix) { + mDisplayMatrix.set(mBaseMatrix); + mDisplayMatrix.postConcat(supportMatrix); + return mDisplayMatrix; + } + + @Override + public void setImageMatrix(Matrix matrix) { + Matrix current = getImageMatrix(); + boolean needUpdate = false; + + if (matrix == null && !current.isIdentity() || matrix != null && !current.equals(matrix)) { + needUpdate = true; + } + + super.setImageMatrix(matrix); + if (needUpdate) { + onImageMatrixChanged(); + } + } + + private void onImageMatrixChanged() { + } + + /** + * Returns the current image display matrix.
    + * This matrix can be used in the next call to the {@link #setImageDrawable(Drawable, Matrix, float, float)} to restore the same + * view state of the previous {@link Bitmap}.
    + * Example: + *

    + *

    +     * Matrix currentMatrix = mImageView.getDisplayMatrix();
    +     * mImageView.setImageBitmap( newBitmap, currentMatrix, ZOOM_INVALID, ZOOM_INVALID );
    +     * 
    + * + * @return the current support matrix + */ + public Matrix getDisplayMatrix() { + return new Matrix(mSuppMatrix); + } + + private void getProperBaseMatrix(Drawable drawable, Matrix matrix, RectF rect) { + float w = mBitmapRect.width(); + float h = mBitmapRect.height(); + float widthScale, heightScale; + + matrix.reset(); + + widthScale = rect.width() / w; + heightScale = rect.height() / h; + float scale = Math.min(widthScale, heightScale); + matrix.postScale(scale, scale); + matrix.postTranslate(rect.left, rect.top); + + float tw = (rect.width() - w * scale) / 2.0f; + float th = (rect.height() - h * scale) / 2.0f; + matrix.postTranslate(tw, th); + printMatrix(matrix); + } + + private float getValue(Matrix matrix, int whichValue) { + matrix.getValues(mMatrixValues); + return mMatrixValues[whichValue]; + } + + private void printMatrix(Matrix matrix) { + float scalex = getValue(matrix, Matrix.MSCALE_X); + float scaley = getValue(matrix, Matrix.MSCALE_Y); + float tx = getValue(matrix, Matrix.MTRANS_X); + float ty = getValue(matrix, Matrix.MTRANS_Y); + Log.d(TAG, "matrix: { x: " + tx + ", y: " + ty + ", scalex: " + scalex + ", scaley: " + scaley + " }"); + } + + RectF getBitmapRect() { + return getBitmapRect(mSuppMatrix); + } + + private RectF getBitmapRect(Matrix supportMatrix) { + Matrix m = getImageViewMatrix(supportMatrix); + m.mapRect(mBitmapRectTmp, mBitmapRect); + return mBitmapRectTmp; + } + + private float getScale(Matrix matrix) { + return getValue(matrix, Matrix.MSCALE_X); + } + + @SuppressLint("Override") + public float getRotation() { + return 0; + } + + float getScale() { + return getScale(mSuppMatrix); + } + + public float getBaseScale() { + return getScale(mBaseMatrix); + } + + private void center(boolean horizontal, boolean vertical) { + final Drawable drawable = getDrawable(); + if (drawable == null) { + return; + } + + RectF rect = getCenter(mSuppMatrix, horizontal, vertical); + + if (rect.left != 0 || rect.top != 0) { + postTranslate(rect.left, rect.top); + } + } + + private RectF getCenter(Matrix supportMatrix, boolean horizontal, boolean vertical) { + final Drawable drawable = getDrawable(); + + if (drawable == null) { + return new RectF(0, 0, 0, 0); + } + + mCenterRect.set(0, 0, 0, 0); + RectF rect = getBitmapRect(supportMatrix); + float height = rect.height(); + float width = rect.width(); + float deltaX = 0, deltaY = 0; + if (vertical) { + if (height < mViewPort.height()) { + deltaY = (mViewPort.height() - height) / 2 - (rect.top - mViewPort.top); + } else if (rect.top > mViewPort.top) { + deltaY = -(rect.top - mViewPort.top); + } else if (rect.bottom < mViewPort.bottom) { + deltaY = mViewPort.bottom - rect.bottom; + } + } + if (horizontal) { + if (width < mViewPort.width()) { + deltaX = (mViewPort.width() - width) / 2 - (rect.left - mViewPort.left); + } else if (rect.left > mViewPort.left) { + deltaX = -(rect.left - mViewPort.left); + } else if (rect.right < mViewPort.right) { + deltaX = mViewPort.right - rect.right; + } + } + mCenterRect.set(deltaX, deltaY, 0, 0); + return mCenterRect; + } + + private void postTranslate(float deltaX, float deltaY) { + if (deltaX != 0 || deltaY != 0) { + mSuppMatrix.postTranslate(deltaX, deltaY); + setImageMatrix(getImageViewMatrix()); + } + } + + private void postScale(float scale, float centerX, float centerY) { + mSuppMatrix.postScale(scale, scale, centerX, centerY); + setImageMatrix(getImageViewMatrix()); + } + + private PointF getCenter() { + return mCenter; + } + + private void zoomTo(float scale) { + if (DEBUG) { + Log.i(TAG, "zoomTo: " + scale); + } + + if (scale > getMaxScale()) { + scale = getMaxScale(); + } + if (scale < getMinScale()) { + scale = getMinScale(); + } + + if (DEBUG) { + Log.d(TAG, "sanitized scale: " + scale); + } + + PointF center = getCenter(); + zoomTo(scale, center.x, center.y); + } + + /** + * Scale to the target scale + * + * @param scale the target zoom + * @param durationMs the animation duration + */ + void zoomTo(float scale, long durationMs) { + PointF center = getCenter(); + zoomTo(scale, center.x, center.y, durationMs); + } + + void zoomTo(float scale, float centerX, float centerY) { + if (scale > getMaxScale()) { + scale = getMaxScale(); + } + + float oldScale = getScale(); + float deltaScale = scale / oldScale; + postScale(deltaScale, centerX, centerY); + onZoom(getScale()); + center(true, true); + } + + @SuppressWarnings("unused") + protected void onZoom(float scale) { + } + + @SuppressWarnings("unused") + protected void onZoomAnimationCompleted(float scale) { + } + + void scrollBy(float x, float y) { + panBy(x, y); + } + + private void panBy(double dx, double dy) { + RectF rect = getBitmapRect(); + mScrollPoint.set((float) dx, (float) dy); + updateRect(rect, mScrollPoint); + + if (mScrollPoint.x != 0 || mScrollPoint.y != 0) { + postTranslate(mScrollPoint.x, mScrollPoint.y); + center(true, true); + } + } + + void updateRect(RectF bitmapRect, PointF scrollRect) { + + } + + void stopAllAnimations() { + if (null != mCurrentAnimation) { + mCurrentAnimation.cancel(); + mCurrentAnimation = null; + } + } + + void scrollBy(float distanceX, float distanceY, final long durationMs) { + final ValueAnimator anim1 = ValueAnimator.ofFloat(0, distanceX).setDuration(durationMs); + final ValueAnimator anim2 = ValueAnimator.ofFloat(0, distanceY).setDuration(durationMs); + + stopAllAnimations(); + + mCurrentAnimation = new AnimatorSet(); + ((AnimatorSet) mCurrentAnimation).playTogether( + anim1, anim2 + ); + + mCurrentAnimation.setDuration(durationMs); + mCurrentAnimation.setInterpolator(new DecelerateInterpolator()); + mCurrentAnimation.start(); + + anim2.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + float oldValueX = 0; + float oldValueY = 0; + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + @Override + public void onAnimationUpdate(final ValueAnimator animation) { + float valueX = (Float) anim1.getAnimatedValue(); + float valueY = (Float) anim2.getAnimatedValue(); + panBy(valueX - oldValueX, valueY - oldValueY); + oldValueX = valueX; + oldValueY = valueY; + postInvalidateOnAnimation(); + } + } + ); + + mCurrentAnimation.addListener( + new Animator.AnimatorListener() { + @Override + public void onAnimationStart(final Animator animation) { + + } + + @Override + public void onAnimationEnd(final Animator animation) { + RectF centerRect = getCenter(mSuppMatrix, true, true); + if (centerRect.left != 0 || centerRect.top != 0) { + scrollBy(centerRect.left, centerRect.top); + } + } + + @Override + public void onAnimationCancel(final Animator animation) { + + } + + @Override + public void onAnimationRepeat(final Animator animation) { + + } + } + ); + } + + void zoomTo(float scale, float centerX, float centerY, final long durationMs) { + if (scale > getMaxScale()) { + scale = getMaxScale(); + } + + final float oldScale = getScale(); + + Matrix m = new Matrix(mSuppMatrix); + m.postScale(scale, scale, centerX, centerY); + RectF rect = getCenter(m, true, true); + + final float finalScale = scale; + final float destX = centerX + rect.left * scale; + final float destY = centerY + rect.top * scale; + + stopAllAnimations(); + + ValueAnimator animation = ValueAnimator.ofFloat(oldScale, finalScale); + animation.setDuration(durationMs); + animation.setInterpolator(new DecelerateInterpolator(1.0f)); + animation.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + @Override + public void onAnimationUpdate(final ValueAnimator animation) { + float value = (Float) animation.getAnimatedValue(); + zoomTo(value, destX, destY); + postInvalidateOnAnimation(); + } + } + ); + animation.start(); + } + + @Override + public void dispose() { + clear(); + } + + @Override + protected void onDraw(final Canvas canvas) { + + if (getScaleType() == ScaleType.FIT_XY) { + final Drawable drawable = getDrawable(); + if (null != drawable) { + drawable.draw(canvas); + } + } else { + super.onDraw(canvas); + } + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/graphics/FastBitmapDrawable.java b/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/graphics/FastBitmapDrawable.java new file mode 100644 index 0000000..1880c5b --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/graphics/FastBitmapDrawable.java @@ -0,0 +1,110 @@ +package mohammadaminha.com.widgets.ImageViewZoom.graphics; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; + +import java.io.InputStream; + +/** + * Fast bitmap drawable. Does not support states. it only + * support alpha and colormatrix + * + * @author alessandro + */ +public class FastBitmapDrawable extends Drawable implements IBitmapDrawable { + private Bitmap mBitmap; + private final Paint mPaint; + private final int mIntrinsicWidth; + private final int mIntrinsicHeight; + + public FastBitmapDrawable(Bitmap b) { + mBitmap = b; + if (null != mBitmap) { + mIntrinsicWidth = mBitmap.getWidth(); + mIntrinsicHeight = mBitmap.getHeight(); + } else { + mIntrinsicWidth = 0; + mIntrinsicHeight = 0; + } + mPaint = new Paint(); + mPaint.setDither(true); + mPaint.setFilterBitmap(true); + } + + public void setBitmap(Bitmap bitmap) { + mBitmap = bitmap; + } + + public FastBitmapDrawable(Resources res, InputStream is) { + this(BitmapFactory.decodeStream(is)); + } + + @Override + public void draw(@NonNull Canvas canvas) { + if (null != mBitmap && !mBitmap.isRecycled()) { + final Rect bounds = getBounds(); + if (!bounds.isEmpty()) { + canvas.drawBitmap(mBitmap, null, bounds, mPaint); + } else { + canvas.drawBitmap(mBitmap, 0f, 0f, mPaint); + } + } + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getIntrinsicWidth() { + return mIntrinsicWidth; + } + + @Override + public int getIntrinsicHeight() { + return mIntrinsicHeight; + } + + @Override + public int getMinimumWidth() { + return mIntrinsicWidth; + } + + @Override + public int getMinimumHeight() { + return mIntrinsicHeight; + } + + public void setAntiAlias(boolean value) { + mPaint.setAntiAlias(value); + invalidateSelf(); + } + + @Override + public Bitmap getBitmap() { + return mBitmap; + } + + public Paint getPaint() { + return mPaint; + } +} \ No newline at end of file diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/graphics/IBitmapDrawable.java b/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/graphics/IBitmapDrawable.java new file mode 100644 index 0000000..b211470 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/graphics/IBitmapDrawable.java @@ -0,0 +1,14 @@ +package mohammadaminha.com.widgets.ImageViewZoom.graphics; + +import android.graphics.Bitmap; + + +/** + * Base interface used in the {@link ImageViewTouchBase} view + * + * @author alessandro + */ +interface IBitmapDrawable { + + Bitmap getBitmap(); +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/utils/IDisposable.java b/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/utils/IDisposable.java new file mode 100644 index 0000000..49b284c --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/ImageViewZoom/utils/IDisposable.java @@ -0,0 +1,6 @@ +package mohammadaminha.com.widgets.ImageViewZoom.utils; + +public interface IDisposable { + + void dispose(); +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/JalaliCalendar.java b/widgets/src/main/java/mohammadaminha/com/widgets/JalaliCalendar.java new file mode 100644 index 0000000..160e453 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/JalaliCalendar.java @@ -0,0 +1,418 @@ +package mohammadaminha.com.widgets; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +public class JalaliCalendar { + private int year, month, day; + + /** + * Today Jalali Date + */ + public JalaliCalendar() { + fromGregorian(new GregorianCalendar()); + } + + /** + * Create a ir.huri.jcal.JalaliCalendar object + * + * @param year Jalali Year + * @param month Jalali Month + * @param day Jalali Day + */ + private JalaliCalendar(int year, int month, int day) { + set(year, month, day); + } + + + /** + * Create a ir.huri.jcal.JalaliCalendar object from gregorian calendar + * + * @param gc gregorian calendar object + */ + public JalaliCalendar(GregorianCalendar gc) { + fromGregorian(gc); + } + + /** + * Convert current jalali date to gregorian date + * + * @return date converted gregorianDate + */ + private GregorianCalendar toGregorian() { + int julianDay = toJulianDay(); + return julianDayToGregorianCalendar(julianDay); + } + + /** + * set date from gregorian date + * + * @param gc input gregorian calendar + */ + private void fromGregorian(GregorianCalendar gc) { + int jd = gregorianToJulianDayNumber(gc); + fromJulianDay(jd); + } + + /** + * @return yesterday date + */ + public JalaliCalendar getYesterday() { + return getDateByDiff(-1); + } + + /** + * @return tomorrow date + */ + public JalaliCalendar getTomorrow() { + return getDateByDiff(1); + } + + /** + * get Jalali date by day difference + * + * @param diff number of day diffrents + * @return jalali calendar diffحزن + */ + private JalaliCalendar getDateByDiff(int diff) { + GregorianCalendar gc = toGregorian(); + gc.add(Calendar.DAY_OF_MONTH, diff); + return new JalaliCalendar(gc); + } + + /** + * @return day Of Week + */ + private int getDayOfWeek() { + return toGregorian().get(Calendar.DAY_OF_WEEK); + } + + /** + * @return get first day of week + */ + public int getFirstDayOfWeek() { + return toGregorian().getFirstDayOfWeek(); + } + + /** + * @return day name + */ + private String getDayOfWeekString() { + switch (getDayOfWeek()) { + case 1: + return "یک‌شنبه"; + case 2: + return "دوشنبه"; + case 3: + return "سه‌شنبه"; + case 4: + return "چهارشنبه"; + case 5: + return "پنجشنبه"; + case 6: + return "جمعه"; + case 7: + return "شنبه"; + default: + return "نامعلوم"; + } + } + + /** + * @return month name + */ + private String getMonthString() { + switch (getMonth()) { + case 1: + return "فروردین"; + case 2: + return "اردیبهشت"; + case 3: + return "خرداد"; + case 4: + return "تیر"; + case 5: + return "مرداد"; + case 6: + return "شهریور"; + case 7: + return "مهر"; + case 8: + return "آبان"; + case 9: + return "آذر"; + case 10: + return "دی"; + case 11: + return "بهمن"; + case 12: + return "اسفند"; + default: + return "نامعلوم"; + } + } + + + /** + * get String with the following format : + * یکشنبه ۱۲ آبان + * + * @return String format + */ + + public String getDayOfWeekDayMonthString() { + return getDayOfWeekString() + " " + getDay() + " " + getMonthString(); + } + + /** + * @return return whether this year is a jalali leap year + */ + private boolean isLeap() { + return getLeapFactor(getYear()) == 0; + } + + public int getYearLength() { + return isLeap() ? 366 : 365; + } + + public int getMonthLength() { + if (getMonth() < 7) { + return 31; + } else if (getMonth() < 12) { + return 30; + } else if (getMonth() == 12) { + if (isLeap()) + return 30; + else + return 29; + } + return 0; + } + + private int getDay() { + return day; + } + + private int getMonth() { + return month; + } + + private int getYear() { + return year; + } + + private void setMonth(int month) { + this.month = month; + } + + private void setYear(int year) { + this.year = year; + } + + private void setDay(int day) { + this.day = day; + } + + private void set(int year, int month, int day) { + setYear(year); + setMonth(month); + setDay(day); + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JalaliCalendar that = (JalaliCalendar) o; + + return year == that.year && month == that.month && day == that.day; + } + + private int gregorianToJulianDayNumber(GregorianCalendar gc) { + int gregorianYear = gc.get(GregorianCalendar.YEAR); + int gregorianMonth = gc.get(GregorianCalendar.MONTH) + 1; + int gregorianDay = gc.get(GregorianCalendar.DAY_OF_MONTH); + + return (((1461 * (gregorianYear + 4800 + (gregorianMonth - 14) / 12)) / 4 + + (367 * (gregorianMonth - 2 - 12 * ((gregorianMonth - 14) / 12))) / 12 + - (3 * ((gregorianYear + 4900 + (gregorianMonth - 14) / 12) / 100)) / 4 + gregorianDay + - 32075) - (gregorianYear + 100100 + (gregorianMonth - 8) / 6) / 100 * 3 / 4 + 752); + } + + private int julianToJulianDayNumber(JulianCalendar jc) { + int julianYear = jc.getYear(); + int julianMonth = jc.getMonth(); + int JulianDay = jc.getDay(); + + return (1461 * (julianYear + 4800 + (julianMonth - 14) / 12)) / 4 + + (367 * (julianMonth - 2 - 12 * ((julianMonth - 14) / 12))) / 12 + - (3 * ((julianYear + 4900 + (julianMonth - 14) / 12) / 100)) / 4 + JulianDay + - 32075; + } + + private GregorianCalendar julianDayToGregorianCalendar(int JulianDayNumber) { + + int j = 4 * JulianDayNumber + 139361631 + (4 * JulianDayNumber + 183187720) / 146097 * 3 / 4 * 4 - 3908; + int i = (j % 1461) / 4 * 5 + 308; + + int gregorianDay = (i % 153) / 5 + 1; + int gregorianMonth = ((i / 153) % 12) + 1; + int gregorianYear = j / 1461 - 100100 + (8 - gregorianMonth) / 6; + + return new GregorianCalendar(gregorianYear, gregorianMonth - 1, gregorianDay); + } + + private void fromJulianDay(int JulianDayNumber) { + GregorianCalendar gc = julianDayToGregorianCalendar(JulianDayNumber); + int gregorianYear = gc.get(GregorianCalendar.YEAR); + + int jalaliYear, jalaliMonth, jalaliDay; + + jalaliYear = gregorianYear - 621; + + GregorianCalendar gregorianFirstFarvardin = new JalaliCalendar(jalaliYear, 1, 1).getGregorianFirstFarvardin(); + int JulianDayFarvardinFirst = gregorianToJulianDayNumber(gregorianFirstFarvardin); + int diffFromFarvardinFirst = JulianDayNumber - JulianDayFarvardinFirst; + + + if (diffFromFarvardinFirst >= 0) { + if (diffFromFarvardinFirst <= 185) { + jalaliMonth = 1 + diffFromFarvardinFirst / 31; + jalaliDay = (diffFromFarvardinFirst % 31) + 1; + set(jalaliYear, jalaliMonth, jalaliDay); + return; + } else { + diffFromFarvardinFirst = diffFromFarvardinFirst - 186; + } + } else { + diffFromFarvardinFirst = diffFromFarvardinFirst + 179; + if (getLeapFactor(jalaliYear) == 1) + diffFromFarvardinFirst = diffFromFarvardinFirst + 1; + jalaliYear -= 1; + } + + + jalaliMonth = 7 + diffFromFarvardinFirst / 30; + jalaliDay = (diffFromFarvardinFirst % 30) + 1; + set(jalaliYear, jalaliMonth, jalaliDay); + } + + private int toJulianDay() { + int jalaliMonth = getMonth(); + int jalaliDay = getDay(); + + GregorianCalendar gregorianFirstFarvardin = getGregorianFirstFarvardin(); + int gregorianYear = gregorianFirstFarvardin.get(Calendar.YEAR); + int gregorianMonth = gregorianFirstFarvardin.get(Calendar.MONTH) + 1; + int gregorianDay = gregorianFirstFarvardin.get(Calendar.DAY_OF_MONTH); + + JulianCalendar julianFirstFarvardin = new JulianCalendar(gregorianYear, gregorianMonth, gregorianDay); + + return julianToJulianDayNumber(julianFirstFarvardin) + (jalaliMonth - 1) * 31 - jalaliMonth / 7 * (jalaliMonth - 7) + + jalaliDay - 1; + } + + + private GregorianCalendar getGregorianFirstFarvardin() { + int marchDay = 0; + int[] breaks = {-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, + 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178}; + + int jalaliYear = getYear(); + int gregorianYear = jalaliYear + 621; + int jalaliLeap = -14; + int jp = breaks[0]; + + int jump ; + for (int j = 1; j <= 19; j++) { + int jm = breaks[j]; + jump = jm - jp; + if (jalaliYear < jm) { + int N = jalaliYear - jp; + jalaliLeap = jalaliLeap + N / 33 * 8 + (N % 33 + 3) / 4; + + if ((jump % 33) == 4 && (jump - N) == 4) + jalaliLeap = jalaliLeap + 1; + + int GregorianLeap = (gregorianYear / 4) - (gregorianYear / 100 + 1) * 3 / 4 - 150; + + marchDay = 20 + (jalaliLeap - GregorianLeap); + + if ((jump - N) < 6) + N = N - jump + (jump + 4) / 33 * 33; + + break; + } + + jalaliLeap = jalaliLeap + jump / 33 * 8 + (jump % 33) / 4; + jp = jm; + } + + return new GregorianCalendar(gregorianYear, 2, marchDay); + } + + private int getLeapFactor(int jalaliYear) { + int leap = 0; + int[] breaks = {-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, + 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178}; + + int jp = breaks[0]; + + int jump ; + for (int j = 1; j <= 19; j++) { + int jm = breaks[j]; + jump = jm - jp; + if (jalaliYear < jm) { + int N = jalaliYear - jp; + + if ((jump - N) < 6) + N = N - jump + (jump + 4) / 33 * 33; + + leap = ((((N + 1) % 33) - 1) % 4); + + if (leap == -1) + leap = 4; + + break; + } + + jp = jm; + } + + return leap; + } + + @Override + public String toString() { + return String.format("%04d-%02d-%02d", getYear(), getMonth(), getDay()); + } + + + private class JulianCalendar { + final int year; + final int month; + final int day; + + public JulianCalendar(int year, int month, int day) { + this.year = year; + this.month = month; + this.day = day; + } + + public int getYear() { + return year; + } + + public int getMonth() { + return month; + } + + public int getDay() { + return day; + } + } + + +} \ No newline at end of file diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/JalaliCalendar1.java b/widgets/src/main/java/mohammadaminha/com/widgets/JalaliCalendar1.java new file mode 100644 index 0000000..c1f5104 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/JalaliCalendar1.java @@ -0,0 +1,802 @@ +package mohammadaminha.com.widgets; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +public class JalaliCalendar1 extends Calendar { + private static final int[] gregorianDaysInMonth = {31, 28, 31, 30, 31, + 30, 31, 31, 30, 31, 30, 31}; + private static final int[] jalaliDaysInMonth = {31, 31, 31, 31, 31, 31, + 30, 30, 30, 30, 30, 29}; + + private static int FARVARDIN = 0; + public final static int ORDIBEHESHT = 1; + public final static int KHORDAD = 2; + public final static int TIR = 3; + public final static int MORDAD = 4; + public final static int SHAHRIVAR = 5; + public final static int MEHR = 6; + public final static int ABAN = 7; + public final static int AZAR = 8; + public final static int DEY = 9; + public final static int BAHMAN = 10; + private static int ESFAND = 11; + + private static TimeZone timeZone = TimeZone.getDefault(); + private static boolean isTimeSeted = false; + + private static final int ONE_SECOND = 1000; + private static final int ONE_MINUTE = 60 * ONE_SECOND; + private static final int ONE_HOUR = 60 * ONE_MINUTE; + private static final long ONE_DAY = 24 * ONE_HOUR; + private static final int BCE = 0; + private static final int CE = 1; + public static final int AD = 1; + private GregorianCalendar cal; + + private static final int[] MIN_VALUES = { + BCE, // ERA + 1, // YEAR + FARVARDIN, // MONTH + 1, // WEEK_OF_YEAR + 0, // WEEK_OF_MONTH + 1, // DAY_OF_MONTH + 1, // DAY_OF_YEAR + SATURDAY, // DAY_OF_WEEK + 1, // DAY_OF_WEEK_IN_MONTH + AM, // AM_PM + 0, // HOUR + 0, // HOUR_OF_DAY + 0, // MINUTE + 0, // SECOND + 0, // MILLISECOND + -13 * ONE_HOUR, // ZONE_OFFSET (UNIX compatibility) + 0 // DST_OFFSET + }; + + private static final int[] LEAST_MAX_VALUES = { + CE, // ERA + 292269054, // YEAR + ESFAND, // MONTH + 52, // WEEK_OF_YEAR + 4, // WEEK_OF_MONTH + 28, // DAY_OF_MONTH + 365, // DAY_OF_YEAR + FRIDAY, // DAY_OF_WEEK + 4, // DAY_OF_WEEK_IN + PM, // AM_PM + 11, // HOUR + 23, // HOUR_OF_DAY + 59, // MINUTE + 59, // SECOND + 999, // MILLISECOND + 14 * ONE_HOUR, // ZONE_OFFSET + 20 * ONE_MINUTE // DST_OFFSET (historical least maximum) + }; + + private static final int[] MAX_VALUES = { + CE, // ERA + 292278994, // YEAR + ESFAND, // MONTH + 53, // WEEK_OF_YEAR + 6, // WEEK_OF_MONTH + 31, // DAY_OF_MONTH + 366, // DAY_OF_YEAR + FRIDAY, // DAY_OF_WEEK + 6, // DAY_OF_WEEK_IN + PM, // AM_PM + 11, // HOUR + 23, // HOUR_OF_DAY + 59, // MINUTE + 59, // SECOND + 999, // MILLISECOND + 14 * ONE_HOUR, // ZONE_OFFSET + 2 * ONE_HOUR // DST_OFFSET (double summer time) + }; + + public JalaliCalendar1() { + this(TimeZone.getDefault(), Locale.getDefault()); + } + + public JalaliCalendar1(TimeZone zone) { + this(zone, Locale.getDefault()); + } + + public JalaliCalendar1(Locale aLocale) { + this(TimeZone.getDefault(), aLocale); + } + + private JalaliCalendar1(TimeZone zone, Locale aLocale) { + + super(zone, aLocale); + timeZone = zone; + Calendar calendar = Calendar.getInstance(zone, aLocale); + + YearMonthDate yearMonthDate = new YearMonthDate(calendar.get(YEAR), calendar.get(MONTH), calendar.get(DATE)); + yearMonthDate = gregorianToJalali(yearMonthDate); + set(yearMonthDate.getYear(), yearMonthDate.getMonth(), yearMonthDate.getDate()); + complete(); + + } + + public JalaliCalendar1(int year, int month, int dayOfMonth) { + this(year, month, dayOfMonth, 0, 0, 0, 0); + } + + public JalaliCalendar1(int year, int month, int dayOfMonth, int hourOfDay, + int minute) { + this(year, month, dayOfMonth, hourOfDay, minute, 0, 0); + } + + public JalaliCalendar1(int year, int month, int dayOfMonth, int hourOfDay, + int minute, int second) { + this(year, month, dayOfMonth, hourOfDay, minute, second, 0); + } + + private JalaliCalendar1(int year, int month, int dayOfMonth, + int hourOfDay, int minute, int second, int millis) { + super(); + + this.set(YEAR, year); + this.set(MONTH, month); + this.set(DAY_OF_MONTH, dayOfMonth); + + if (hourOfDay >= 12 && hourOfDay <= 23) { + + this.set(AM_PM, PM); + this.set(HOUR, hourOfDay - 12); + } else { + this.set(HOUR, hourOfDay); + this.set(AM_PM, AM); + } + + this.set(HOUR_OF_DAY, hourOfDay); + this.set(MINUTE, minute); + this.set(SECOND, second); + + this.set(MILLISECOND, millis); + + YearMonthDate yearMonthDate = jalaliToGregorian(new YearMonthDate(fields[1], fields[2], fields[5])); + cal = new GregorianCalendar(yearMonthDate.getYear(), yearMonthDate.getMonth(), yearMonthDate.getDate(), hourOfDay, + minute, second); + time = cal.getTimeInMillis(); + + isTimeSeted = true; + } + + + private static YearMonthDate gregorianToJalali(YearMonthDate gregorian) { + + if (gregorian.getMonth() > 11 || gregorian.getMonth() < -11) { + throw new IllegalArgumentException(); + } + int jalaliYear; + int jalaliMonth; + int jalaliDay; + + int gregorianDayNo, jalaliDayNo; + int jalaliNP; + int i; + + gregorian.setYear(gregorian.getYear() - 1600); + gregorian.setDate(gregorian.getDate() - 1); + + gregorianDayNo = 365 * gregorian.getYear() + (int) Math.floor((gregorian.getYear() + 3) / 4) + - (int) Math.floor((gregorian.getYear() + 99) / 100) + + (int) Math.floor((gregorian.getYear() + 399) / 400); + for (i = 0; i < gregorian.getMonth(); ++i) { + gregorianDayNo += gregorianDaysInMonth[i]; + } + + if (gregorian.getMonth() > 1 && ((gregorian.getYear() % 4 == 0 && gregorian.getYear() % 100 != 0) + || (gregorian.getYear() % 400 == 0))) { + ++gregorianDayNo; + } + + gregorianDayNo += gregorian.getDate(); + + jalaliDayNo = gregorianDayNo - 79; + + jalaliNP = (int) Math.floor(jalaliDayNo / 12053); + jalaliDayNo = jalaliDayNo % 12053; + + jalaliYear = 979 + 33 * jalaliNP + 4 * jalaliDayNo / 1461; + jalaliDayNo = jalaliDayNo % 1461; + + if (jalaliDayNo >= 366) { + jalaliYear += (int) Math.floor((jalaliDayNo - 1) / 365); + jalaliDayNo = (jalaliDayNo - 1) % 365; + } + + for (i = 0; i < 11 && jalaliDayNo >= jalaliDaysInMonth[i]; ++i) { + jalaliDayNo -= jalaliDaysInMonth[i]; + } + jalaliMonth = i; + jalaliDay = jalaliDayNo + 1; + + return new YearMonthDate(jalaliYear, jalaliMonth, jalaliDay); + } + + + private static YearMonthDate jalaliToGregorian(YearMonthDate jalali) { + if (jalali.getMonth() > 11 || jalali.getMonth() < -11) { + throw new IllegalArgumentException(); + } + + int gregorianYear; + int gregorianMonth; + int gregorianDay; + + int gregorianDayNo, jalaliDayNo; + int leap; + + int i; + jalali.setYear(jalali.getYear() - 979); + jalali.setDate(jalali.getDate() - 1); + + jalaliDayNo = 365 * jalali.getYear() + jalali.getYear() / 33 * 8 + + (int) Math.floor(((jalali.getYear() % 33) + 3) / 4); + for (i = 0; i < jalali.getMonth(); ++i) { + jalaliDayNo += jalaliDaysInMonth[i]; + } + + jalaliDayNo += jalali.getDate(); + + gregorianDayNo = jalaliDayNo + 79; + + gregorianYear = 1600 + 400 * (int) Math.floor(gregorianDayNo / 146097); /* 146097 = 365*400 + 400/4 - 400/100 + 400/400 */ + gregorianDayNo = gregorianDayNo % 146097; + + leap = 1; + if (gregorianDayNo >= 36525) /* 36525 = 365*100 + 100/4 */ { + gregorianDayNo--; + gregorianYear += 100 * (int) Math.floor(gregorianDayNo / 36524); /* 36524 = 365*100 + 100/4 - 100/100 */ + gregorianDayNo = gregorianDayNo % 36524; + + if (gregorianDayNo >= 365) { + gregorianDayNo++; + } else { + leap = 0; + } + } + + gregorianYear += 4 * (int) Math.floor(gregorianDayNo / 1461); /* 1461 = 365*4 + 4/4 */ + gregorianDayNo = gregorianDayNo % 1461; + + if (gregorianDayNo >= 366) { + leap = 0; + + gregorianDayNo--; + gregorianYear += (int) Math.floor(gregorianDayNo / 365); + gregorianDayNo = gregorianDayNo % 365; + } + + for (i = 0; gregorianDayNo >= gregorianDaysInMonth[i] + ((i == 1 && leap == 1) ? i : 0); i++) { + gregorianDayNo -= gregorianDaysInMonth[i] + ((i == 1 && leap == 1) ? i : 0); + } + gregorianMonth = i; + gregorianDay = gregorianDayNo + 1; + + return new YearMonthDate(gregorianYear, gregorianMonth, gregorianDay); + + } + + private static int weekOfYear(int dayOfYear, int year) { + switch (dayOfWeek(JalaliCalendar1.jalaliToGregorian(new YearMonthDate(year, 0, 1)))) { + case 2: + dayOfYear++; + break; + case 3: + dayOfYear += 2; + break; + case 4: + dayOfYear += 3; + break; + case 5: + dayOfYear += 4; + break; + case 6: + dayOfYear += 5; + break; + case 7: + dayOfYear--; + break; + } + dayOfYear = (int) Math.floor(dayOfYear / 7); + return dayOfYear + 1; + } + + private static int dayOfWeek(YearMonthDate yearMonthDate) { + + Calendar cal = new GregorianCalendar(yearMonthDate.getYear(), yearMonthDate.getMonth(), yearMonthDate.getDate()); + return cal.get(DAY_OF_WEEK); + + } + + private static boolean isLeepYear(int year) { + //Algorithm from www.wikipedia.com + return (year % 33 == 1 || year % 33 == 5 || year % 33 == 9 || year % 33 == 13 || + year % 33 == 17 || year % 33 == 22 || year % 33 == 26 || year % 33 == 30); + } + + @Override + protected void computeTime() { + + if (!isTimeSet && !isTimeSeted) { + Calendar cal = GregorianCalendar.getInstance(timeZone); + if (!isSet(HOUR_OF_DAY)) { + super.set(HOUR_OF_DAY, cal.get(HOUR_OF_DAY)); + } + if (!isSet(HOUR)) { + super.set(HOUR, cal.get(HOUR)); + } + if (!isSet(MINUTE)) { + super.set(MINUTE, cal.get(MINUTE)); + } + if (!isSet(SECOND)) { + super.set(SECOND, cal.get(SECOND)); + } + if (!isSet(MILLISECOND)) { + super.set(MILLISECOND, cal.get(MILLISECOND)); + } + if (!isSet(ZONE_OFFSET)) { + super.set(ZONE_OFFSET, cal.get(ZONE_OFFSET)); + } + if (!isSet(DST_OFFSET)) { + super.set(DST_OFFSET, cal.get(DST_OFFSET)); + } + if (!isSet(AM_PM)) { + super.set(AM_PM, cal.get(AM_PM)); + } + + if (internalGet(HOUR_OF_DAY) >= 12 && internalGet(HOUR_OF_DAY) <= 23) { + super.set(AM_PM, PM); + super.set(HOUR, internalGet(HOUR_OF_DAY) - 12); + } else { + super.set(HOUR, internalGet(HOUR_OF_DAY)); + super.set(AM_PM, AM); + } + + YearMonthDate yearMonthDate = jalaliToGregorian(new YearMonthDate(internalGet(YEAR), internalGet(MONTH), internalGet(DAY_OF_MONTH))); + cal.set(yearMonthDate.getYear(), yearMonthDate.getMonth(), yearMonthDate.getDate() + , internalGet(HOUR_OF_DAY), internalGet(MINUTE), internalGet(SECOND)); + time = cal.getTimeInMillis(); + + } else if (!isTimeSet && isTimeSeted) { + if (internalGet(HOUR_OF_DAY) >= 12 && internalGet(HOUR_OF_DAY) <= 23) { + super.set(AM_PM, PM); + super.set(HOUR, internalGet(HOUR_OF_DAY) - 12); + } else { + super.set(HOUR, internalGet(HOUR_OF_DAY)); + super.set(AM_PM, AM); + } + cal = new GregorianCalendar(); + super.set(ZONE_OFFSET, timeZone.getRawOffset()); + super.set(DST_OFFSET, timeZone.getDSTSavings()); + YearMonthDate yearMonthDate = jalaliToGregorian(new YearMonthDate(internalGet(YEAR), internalGet(MONTH), internalGet(DAY_OF_MONTH))); + cal.set(yearMonthDate.getYear(), yearMonthDate.getMonth(), yearMonthDate.getDate(), internalGet(HOUR_OF_DAY), + internalGet(MINUTE), internalGet(SECOND)); + time = cal.getTimeInMillis(); + } + } + + public void set(int field, int value) { + switch (field) { + case DATE: { + super.set(field, 0); + add(field, value); + break; + } + case MONTH: { + if (value > 11) { + super.set(field, 11); + add(field, value - 11); + } else if (value < 0) { + super.set(field, 0); + add(field, value); + } else { + super.set(field, value); + } + break; + } + case DAY_OF_YEAR: { + if (isSet(YEAR) && isSet(MONTH) && isSet(DAY_OF_MONTH)) { + super.set(YEAR, internalGet(YEAR)); + super.set(MONTH, 0); + super.set(DATE, 0); + add(field, value); + } else { + super.set(field, value); + } + break; + } + case WEEK_OF_YEAR: { + if (isSet(YEAR) && isSet(MONTH) && isSet(DAY_OF_MONTH)) { + add(field, value - get(WEEK_OF_YEAR)); + } else { + super.set(field, value); + } + break; + } + case WEEK_OF_MONTH: { + if (isSet(YEAR) && isSet(MONTH) && isSet(DAY_OF_MONTH)) { + add(field, value - get(WEEK_OF_MONTH)); + } else { + super.set(field, value); + } + break; + } + case DAY_OF_WEEK: { + if (isSet(YEAR) && isSet(MONTH) && isSet(DAY_OF_MONTH)) { + add(DAY_OF_WEEK, value % 7 - get(DAY_OF_WEEK)); + } else { + super.set(field, value); + } + break; + } + case HOUR_OF_DAY: + case HOUR: + case MINUTE: + case SECOND: + case MILLISECOND: + case ZONE_OFFSET: + case DST_OFFSET: { + if (isSet(YEAR) && isSet(MONTH) && isSet(DATE) && isSet(HOUR) && isSet(HOUR_OF_DAY) && + isSet(MINUTE) && isSet(SECOND) && isSet(MILLISECOND)) { + cal = new GregorianCalendar(); + YearMonthDate yearMonthDate = jalaliToGregorian(new YearMonthDate(internalGet(YEAR), internalGet(MONTH), internalGet(DATE))); + cal.set(yearMonthDate.getYear(), yearMonthDate.getMonth(), yearMonthDate.getDate(), internalGet(HOUR_OF_DAY), internalGet(MINUTE), + internalGet(SECOND)); + cal.set(field, value); + yearMonthDate = gregorianToJalali(new YearMonthDate(cal.get(YEAR), cal.get(MONTH), cal.get(DATE))); + super.set(YEAR, yearMonthDate.getYear()); + super.set(MONTH, yearMonthDate.getMonth()); + super.set(DATE, yearMonthDate.getDate()); + super.set(HOUR_OF_DAY, cal.get(HOUR_OF_DAY)); + super.set(MINUTE, cal.get(MINUTE)); + super.set(SECOND, cal.get(SECOND)); + + } else { + super.set(field, value); + } + break; + } + + + default: { + super.set(field, value); + } + } + } + + @Override + protected void computeFields() { + boolean temp = isTimeSet; + if (!areFieldsSet) { + setMinimalDaysInFirstWeek(1); + setFirstDayOfWeek(7); + + //Day_Of_Year + int dayOfYear = 0; + int index = 0; + + while (index < fields[2]) { + dayOfYear += jalaliDaysInMonth[index++]; + } + dayOfYear += fields[5]; + super.set(DAY_OF_YEAR, dayOfYear); + //*** + + //Day_of_Week + super.set(DAY_OF_WEEK, dayOfWeek(jalaliToGregorian(new YearMonthDate(fields[1], fields[2], fields[5])))); + //*** + + //Day_Of_Week_In_Month + if (0 < fields[5] && fields[5] < 8) { + super.set(DAY_OF_WEEK_IN_MONTH, 1); + } + + if (7 < fields[5] && fields[5] < 15) { + super.set(DAY_OF_WEEK_IN_MONTH, 2); + } + + if (14 < fields[5] && fields[5] < 22) { + super.set(DAY_OF_WEEK_IN_MONTH, 3); + } + + if (21 < fields[5] && fields[5] < 29) { + super.set(DAY_OF_WEEK_IN_MONTH, 4); + } + + if (28 < fields[5] && fields[5] < 32) { + super.set(DAY_OF_WEEK_IN_MONTH, 5); + } + //*** + + //Week_Of_Year + super.set(WEEK_OF_YEAR, weekOfYear(fields[6], fields[1])); + //*** + + //Week_Of_Month + super.set(WEEK_OF_MONTH, weekOfYear(fields[6], fields[1]) - weekOfYear(fields[6] - fields[5], fields[1]) + 1); + // + + isTimeSet = temp; + } + } + + @Override + public void add(int field, int amount) { + + if (field == MONTH) { + amount += get(MONTH); + add(YEAR, amount / 12); + super.set(MONTH, amount % 12); + if (get(DAY_OF_MONTH) > jalaliDaysInMonth[amount % 12]) { + super.set(DAY_OF_MONTH, jalaliDaysInMonth[amount % 12]); + if (get(MONTH) == 11 && isLeepYear(get(YEAR))) { + super.set(DAY_OF_MONTH, 30); + } + } + complete(); + + } else if (field == YEAR) { + + super.set(YEAR, get(YEAR) + amount); + if (get(DAY_OF_MONTH) == 30 && get(MONTH) == 11 && !isLeepYear(get(YEAR))) { + super.set(DAY_OF_MONTH, 29); + } + + complete(); + } else { + YearMonthDate yearMonthDate = jalaliToGregorian(new YearMonthDate(get(YEAR), get(MONTH), get(DATE))); + Calendar gc = new GregorianCalendar(yearMonthDate.getYear(), yearMonthDate.getMonth(), yearMonthDate.getDate(), + get(HOUR_OF_DAY), get(MINUTE), get(SECOND)); + gc.add(field, amount); + yearMonthDate = gregorianToJalali(new YearMonthDate(gc.get(YEAR), gc.get(MONTH), gc.get(DATE))); + super.set(YEAR, yearMonthDate.getYear()); + super.set(MONTH, yearMonthDate.getMonth()); + super.set(DATE, yearMonthDate.getDate()); + super.set(HOUR_OF_DAY, gc.get(HOUR_OF_DAY)); + super.set(MINUTE, gc.get(MINUTE)); + super.set(SECOND, gc.get(SECOND)); + complete(); + } + + } + + @Override + public void roll(int field, boolean up) { + roll(field, up ? +1 : -1); + } + + @Override + public void roll(int field, int amount) { + if (amount == 0) { + return; + } + + if (field < 0 || field >= ZONE_OFFSET) { + throw new IllegalArgumentException(); + } + + complete(); + + switch (field) { + case AM_PM: { + if (amount % 2 != 0) { + if (internalGet(AM_PM) == AM) { + fields[AM_PM] = PM; + } else { + fields[AM_PM] = AM; + } + if (get(AM_PM) == AM) { + super.set(HOUR_OF_DAY, get(HOUR)); + } else { + super.set(HOUR_OF_DAY, get(HOUR) + 12); + } + } + break; + } + case YEAR: { + super.set(YEAR, internalGet(YEAR) + amount); + if (internalGet(MONTH) == 11 && internalGet(DAY_OF_MONTH) == 30 && !isLeepYear(internalGet(YEAR))) { + super.set(DAY_OF_MONTH, 29); + } + break; + } + case MINUTE: { + int unit = 60; + int m = (internalGet(MINUTE) + amount) % unit; + if (m < 0) { + m += unit; + } + super.set(MINUTE, m); + break; + } + case SECOND: { + int unit = 60; + int s = (internalGet(SECOND) + amount) % unit; + if (s < 0) { + s += unit; + } + super.set(SECOND, s); + break; + } + case MILLISECOND: { + int unit = 1000; + int ms = (internalGet(MILLISECOND) + amount) % unit; + if (ms < 0) { + ms += unit; + } + super.set(MILLISECOND, ms); + break; + } + + case HOUR: { + super.set(HOUR, (internalGet(HOUR) + amount) % 12); + if (internalGet(HOUR) < 0) { + fields[HOUR] += 12; + } + if (internalGet(AM_PM) == AM) { + super.set(HOUR_OF_DAY, internalGet(HOUR)); + } else { + super.set(HOUR_OF_DAY, internalGet(HOUR) + 12); + } + + break; + } + case HOUR_OF_DAY: { + fields[HOUR_OF_DAY] = (internalGet(HOUR_OF_DAY) + amount) % 24; + if (internalGet(HOUR_OF_DAY) < 0) { + fields[HOUR_OF_DAY] += 24; + } + if (internalGet(HOUR_OF_DAY) < 12) { + fields[AM_PM] = AM; + fields[HOUR] = internalGet(HOUR_OF_DAY); + } else { + fields[AM_PM] = PM; + fields[HOUR] = internalGet(HOUR_OF_DAY) - 12; + } + + } + case MONTH: { + int mon = (internalGet(MONTH) + amount) % 12; + if (mon < 0) { + mon += 12; + } + super.set(MONTH, mon); + + int monthLen = jalaliDaysInMonth[mon]; + if (internalGet(MONTH) == 11 && isLeepYear(internalGet(YEAR))) { + monthLen = 30; + } + if (internalGet(DAY_OF_MONTH) > monthLen) { + super.set(DAY_OF_MONTH, monthLen); + } + break; + } + case DAY_OF_MONTH: { + int unit = 0; + if (0 <= get(MONTH) && get(MONTH) <= 5) { + unit = 31; + } + if (6 <= get(MONTH) && get(MONTH) <= 10) { + unit = 30; + } + if (get(MONTH) == 11) { + if (isLeepYear(get(YEAR))) { + unit = 30; + } else { + unit = 29; + } + } + int d = (get(DAY_OF_MONTH) + amount) % unit; + if (d < 0) { + d += unit; + } + super.set(DAY_OF_MONTH, d); + break; + + } + case WEEK_OF_YEAR: { + break; + } + case DAY_OF_YEAR: { + int unit = (isLeepYear(internalGet(YEAR)) ? 366 : 365); + int dayOfYear = (internalGet(DAY_OF_YEAR) + amount) % unit; + dayOfYear = (dayOfYear > 0) ? dayOfYear : dayOfYear + unit; + int month = 0, temp = 0; + while (dayOfYear > temp) { + temp += jalaliDaysInMonth[month++]; + } + super.set(MONTH, --month); + super.set(DAY_OF_MONTH, jalaliDaysInMonth[internalGet(MONTH)] - (temp - dayOfYear)); + break; + } + case DAY_OF_WEEK: { + int index = amount % 7; + if (index < 0) { + index += 7; + } + int i = 0; + while (i != index) { + if (internalGet(DAY_OF_WEEK) == FRIDAY) { + add(DAY_OF_MONTH, -6); + } else { + add(DAY_OF_MONTH, +1); + } + i++; + } + break; + } + + default: + throw new IllegalArgumentException(); + } + + } + + @Override + public int getMinimum(int field) { + return MIN_VALUES[field]; + } + + @Override + public int getMaximum(int field) { + return MAX_VALUES[field]; + } + + @Override + public int getGreatestMinimum(int field) { + return MIN_VALUES[field]; + } + + @Override + public int getLeastMaximum(int field) { + return LEAST_MAX_VALUES[field]; + } + + public static class YearMonthDate { + + public YearMonthDate(int year, int month, int date) { + this.year = year; + this.month = month; + this.date = date; + } + + private int year; + private int month; + private int date; + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } + + public int getMonth() { + return month; + } + + public void setMonth(int month) { + this.month = month; + } + + public int getDate() { + return date; + } + + public void setDate(int date) { + this.date = date; + } + + public String toString() { + return getYear() + "/" + getMonth() + "/" + getDate(); + } + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/RadioButton.java b/widgets/src/main/java/mohammadaminha/com/widgets/RadioButton.java new file mode 100644 index 0000000..10b79dc --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/RadioButton.java @@ -0,0 +1,35 @@ +package mohammadaminha.com.widgets; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * Created by amin on 2/21/18. + */ + +public class RadioButton extends android.support.v7.widget.AppCompatRadioButton { + + public RadioButton(Context context) { + super(context); + } + + public RadioButton(Context context, AttributeSet attrs) { + super(context, attrs); + setTf(context); + } + + public RadioButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setTf(context); + } + + + + private void setTf(Context context) { + + setTypeface(Util.getTypeFace()); + } + + +} + diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Spinner/OnSpinerItemClick.java b/widgets/src/main/java/mohammadaminha/com/widgets/Spinner/OnSpinerItemClick.java new file mode 100644 index 0000000..c30086b --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Spinner/OnSpinerItemClick.java @@ -0,0 +1,9 @@ +package mohammadaminha.com.widgets.Spinner; + + +import org.json.JSONException; + +public interface OnSpinerItemClick +{ + void onClick(String item, int position) throws JSONException; +} \ No newline at end of file diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Switch.java b/widgets/src/main/java/mohammadaminha/com/widgets/Switch.java new file mode 100644 index 0000000..45b1f3f --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Switch.java @@ -0,0 +1,33 @@ +package mohammadaminha.com.widgets; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * Created by aj on 2/6/2018. + */ + +public class Switch extends android.widget.Switch { + + public Switch(Context context) { + super(context); + setTf(context); + } + + public Switch(Context context, AttributeSet attrs) { + super(context, attrs); + setTf(context); + } + + public Switch(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setTf(context); + } + private void setTf(Context context) { + + setTypeface(Util.getTypeFace()); + + } + + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/TextInputLayout.java b/widgets/src/main/java/mohammadaminha/com/widgets/TextInputLayout.java new file mode 100644 index 0000000..196a7cf --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/TextInputLayout.java @@ -0,0 +1,34 @@ +package mohammadaminha.com.widgets; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * Created by aj on 2/5/2018. + */ + +public class TextInputLayout extends android.support.design.widget.TextInputLayout { + + + public TextInputLayout(Context context) { + super(context); + setTf(context); + } + + public TextInputLayout(Context context, AttributeSet attrs) { + super(context, attrs); + setTf(context); + } + + public TextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setTf(context); + } + + private void setTf(Context context) { + + setTypeface(Util.getTypeFace()); + } + + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/TextView.java b/widgets/src/main/java/mohammadaminha/com/widgets/TextView.java new file mode 100644 index 0000000..baa2a57 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/TextView.java @@ -0,0 +1,41 @@ +package mohammadaminha.com.widgets; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * Created by aj on 1/30/2018. + */ + +public class TextView extends android.support.v7.widget.AppCompatTextView { + + + public TextView(Context context) { + super(context); + setTf(context); + } + + public TextView(Context context, AttributeSet attrs) { + super(context, attrs); + setTf(context); + } + + public TextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setTf(context); + } + + private void setTf(Context context) { + + setTypeface(Util.getTypeFace()); + + } + + + + public void setText(Context context) { + } + + public void setText() { + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/ToolbarCustomizer.java b/widgets/src/main/java/mohammadaminha/com/widgets/ToolbarCustomizer.java new file mode 100644 index 0000000..8ef21ec --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/ToolbarCustomizer.java @@ -0,0 +1,28 @@ +package mohammadaminha.com.widgets; + +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.View; + +/** + * Created by pc3 on 4/17/2018. + */ + +public class ToolbarCustomizer { + public static void Toolbar(final AppCompatActivity activity, Toolbar toolbar) { + activity.setSupportActionBar(toolbar); + activity.getSupportActionBar().setHomeButtonEnabled(true); + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowTitleEnabled(false); + + toolbar.getNavigationIcon().setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP); + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + activity.finish(); + } + }); + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/Util.java b/widgets/src/main/java/mohammadaminha/com/widgets/Util.java new file mode 100644 index 0000000..e71b56c --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/Util.java @@ -0,0 +1,28 @@ +package mohammadaminha.com.widgets; + +import android.content.Context; +import android.graphics.Typeface; + +public class Util { + private static String fontAddress = ""; + private static Typeface typeFace; + private static Context context; + + public Util(String address, Context cnx) { + fontAddress = address; + context = cnx; + typeFace = Typeface.createFromAsset(context.getAssets(), Util.getAddress()); + } + + public static String getAddress() { + return fontAddress; + } + + public static Typeface getTypeFace() { + return typeFace; + } + + public static Context getContext() { + return context; + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/cToast.java b/widgets/src/main/java/mohammadaminha/com/widgets/cToast.java new file mode 100644 index 0000000..49ee9b5 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/cToast.java @@ -0,0 +1,40 @@ +package mohammadaminha.com.widgets; + +import android.app.Activity; +import android.content.Context; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.widget.LinearLayout; +import android.widget.Toast; + +/** + * Created by amin on 1/18/18. + */ + +public class cToast { + /** + * برای تعیین مدت زمان نمایش پیام + * + * @param ToastLengh 0_Toast.LENGTH_SHORT + * 1_Toast.LENGTH_LONG + */ + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1) + public static void show(Activity activity, String message, int ToastLengh) { + Toast toast = Toast.makeText(activity, message, ToastLengh); + LinearLayout toastLayout = (LinearLayout) toast.getView(); + android.widget.TextView toastTV = (android.widget.TextView) toastLayout.getChildAt(0); + toastTV.setText(message); + toastTV.setTypeface(Util.getTypeFace()); + toast.show(); + } + + public static void show(Context context, String message, int ToastLengh) { + Toast toast = Toast.makeText(context, message, ToastLengh); + LinearLayout toastLayout = (LinearLayout) toast.getView(); + android.widget.TextView toastTV = (android.widget.TextView) toastLayout.getChildAt(0); + toastTV.setText(message); + toastTV.setTypeface(Util.getTypeFace()); + toast.show(); + } + +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/particleview/LineEvaluator.java b/widgets/src/main/java/mohammadaminha/com/widgets/particleview/LineEvaluator.java new file mode 100644 index 0000000..d5429b5 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/particleview/LineEvaluator.java @@ -0,0 +1,19 @@ +package mohammadaminha.com.widgets.particleview; + +import android.animation.TypeEvaluator; + +/** + * 作者: 巴掌 on 16/8/27 12:06 + * Github: https://github.com/JeasonWong + */ +public class LineEvaluator implements TypeEvaluator { + + @Override + public Particle evaluate(float fraction, Particle startValue, Particle endValue) { + Particle particle = new Particle(); + particle.x = startValue.x + (endValue.x - startValue.x) * fraction; + particle.y = startValue.y + (endValue.y - startValue.y) * fraction; + particle.radius = startValue.radius + (endValue.radius - startValue.radius) * fraction; + return particle; + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/particleview/Particle.java b/widgets/src/main/java/mohammadaminha/com/widgets/particleview/Particle.java new file mode 100644 index 0000000..8b264f9 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/particleview/Particle.java @@ -0,0 +1,21 @@ +package mohammadaminha.com.widgets.particleview; + +/** + * 作者: 巴掌 on 16/8/27 12:07 + * Github: https://github.com/JeasonWong + */ +public class Particle { + + public float x; + public float y; + public float radius; + + public Particle() { + } + + public Particle(float x, float y, float radius) { + this.x = x; + this.y = y; + this.radius = radius; + } +} diff --git a/widgets/src/main/java/mohammadaminha/com/widgets/particleview/ParticleView.java b/widgets/src/main/java/mohammadaminha/com/widgets/particleview/ParticleView.java new file mode 100644 index 0000000..a5a3959 --- /dev/null +++ b/widgets/src/main/java/mohammadaminha/com/widgets/particleview/ParticleView.java @@ -0,0 +1,385 @@ +package mohammadaminha.com.widgets.particleview; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.view.View; + +import java.util.ArrayList; +import java.util.Collection; + +import mohammadaminha.com.widgets.R; +import mohammadaminha.com.widgets.Util; + +/** + * 作者: 巴掌 on 16/8/27 11:29 + * Github: https://github.com/JeasonWong + */ +public class ParticleView extends View { + + private final int STATUS_MOTIONLESS = 0; + private final int STATUS_PARTICLE_GATHER = 1; + private final int STATUS_TEXT_MOVING = 2; + + private final int ROW_NUM = 10; + private final int COLUMN_NUM = 10; + + private final int DEFAULT_MAX_TEXT_SIZE = sp2px(80); + private final int DEFAULT_MIN_TEXT_SIZE = sp2px(30); + + public final int DEFAULT_TEXT_ANIM_TIME = 1000; + public final int DEFAULT_SPREAD_ANIM_TIME = 300; + public final int DEFAULT_HOST_TEXT_ANIM_TIME = 800; + + public Paint mHostTextPaint; + private Paint mParticleTextPaint; + private Paint mCirclePaint; + private Paint mHostBgPaint; + private int mWidth, mHeight; + + private Particle[][] mParticles = new Particle[ROW_NUM][COLUMN_NUM]; + private Particle[][] mMinParticles = new Particle[ROW_NUM][COLUMN_NUM]; + + //背景色 + private int mBgColor; + //粒子色 + private int mParticleColor; + //默认粒子文案大小 + private int mParticleTextSize = DEFAULT_MIN_TEXT_SIZE; + + private int mStatus = STATUS_MOTIONLESS; + + private ParticleAnimListener mParticleAnimListener; + + //粒子文案 + private String mParticleText; + //主文案 + public String mHostText; + //扩散宽度 + private float mSpreadWidth; + //Host文字展现宽度 + private float mHostRectWidth; + //粒子文案的x坐标 + private float mParticleTextX; + //Host文字的x坐标 + private float mHostTextX; + + //Text anim time in milliseconds + private int mTextAnimTime; + //Spread anim time in milliseconds + private int mSpreadAnimTime; + //HostText anim time in milliseconds + private int mHostTextAnimTime; + + private PointF mStartMaxP, mEndMaxP; + private PointF mStartMinP, mEndMinP; + Typeface bold; + public ParticleView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ParticleView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(attrs); + } + + private void initView(AttributeSet attrs) { + + bold = Typeface.create(Util.getTypeFace(), Typeface.BOLD); + + TypedArray typeArray = getContext().obtainStyledAttributes(attrs, R.styleable.ParticleView); + mHostText = null == typeArray.getString(R.styleable.ParticleView_pv_host_text) ? "" : typeArray.getString(R.styleable.ParticleView_pv_host_text); + mParticleText = null == typeArray.getString(R.styleable.ParticleView_pv_particle_text) ? "" : typeArray.getString(R.styleable.ParticleView_pv_particle_text); + mParticleTextSize = (int) typeArray.getDimension(R.styleable.ParticleView_pv_particle_text_size, DEFAULT_MIN_TEXT_SIZE); + int hostTextSize = (int) typeArray.getDimension(R.styleable.ParticleView_pv_host_text_size, DEFAULT_MIN_TEXT_SIZE); + mBgColor = typeArray.getColor(R.styleable.ParticleView_pv_background_color, 0xFF0867AB); + mParticleColor = typeArray.getColor(R.styleable.ParticleView_pv_text_color, 0xFFCEF4FD); + mTextAnimTime = typeArray.getInt(R.styleable.ParticleView_pv_text_anim_time, DEFAULT_TEXT_ANIM_TIME); + mSpreadAnimTime = typeArray.getInt(R.styleable.ParticleView_pv_text_anim_time, DEFAULT_SPREAD_ANIM_TIME); + mHostTextAnimTime = typeArray.getInt(R.styleable.ParticleView_pv_text_anim_time, DEFAULT_HOST_TEXT_ANIM_TIME); + typeArray.recycle(); + + mHostTextPaint = new Paint(); + mHostTextPaint.setAntiAlias(true); + mHostTextPaint.setTextSize(hostTextSize); + + + mParticleTextPaint = new Paint(); + mParticleTextPaint.setAntiAlias(true); + mCirclePaint = new Paint(); + mCirclePaint.setAntiAlias(true); + mHostBgPaint = new Paint(); + mHostBgPaint.setAntiAlias(true); + mHostBgPaint.setTextSize(hostTextSize); + + mParticleTextPaint.setTextSize(mParticleTextSize); + mCirclePaint.setTextSize(mParticleTextSize); + + mParticleTextPaint.setColor(mBgColor); + mHostTextPaint.setColor(mBgColor); + mCirclePaint.setColor(mParticleColor); + mHostBgPaint.setColor(mParticleColor); + + mHostTextPaint.setTypeface(bold); + mHostBgPaint.setTypeface(bold); + mParticleTextPaint.setTypeface(bold); + + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mWidth = w; + mHeight = h; + + mStartMinP = new PointF(mWidth / 2 - getTextWidth(mParticleText, mParticleTextPaint) / 2f - dip2px(4), mHeight / 2 + getTextHeight(mHostText, mHostTextPaint) / 2 - getTextHeight(mParticleText, mParticleTextPaint) / 0.7f); + mEndMinP = new PointF(mWidth / 2 + getTextWidth(mParticleText, mParticleTextPaint) / 2f + dip2px(10), mHeight / 2 + getTextHeight(mHostText, mHostTextPaint) / 2); + + for (int i = 0; i < ROW_NUM; i++) { + for (int j = 0; j < COLUMN_NUM; j++) { + mMinParticles[i][j] = new Particle(mStartMinP.x + (mEndMinP.x - mStartMinP.x) / COLUMN_NUM * j, mStartMinP.y + (mEndMinP.y - mStartMinP.y) / ROW_NUM * i, dip2px(0.8f)); + } + } + + mStartMaxP = new PointF(mWidth / 2 - DEFAULT_MAX_TEXT_SIZE, mHeight / 2 - DEFAULT_MAX_TEXT_SIZE); + mEndMaxP = new PointF(mWidth / 2 + DEFAULT_MAX_TEXT_SIZE, mHeight / 2 + DEFAULT_MAX_TEXT_SIZE); + + for (int i = 0; i < ROW_NUM; i++) { + for (int j = 0; j < COLUMN_NUM; j++) { + mParticles[i][j] = new Particle(mStartMaxP.x + (mEndMaxP.x - mStartMaxP.x) / COLUMN_NUM * j, mStartMaxP.y + (mEndMaxP.y - mStartMaxP.y) / ROW_NUM * i, getTextWidth(mHostText + mParticleText, mParticleTextPaint) / (COLUMN_NUM * 1.8f)); + } + } + + Shader linearGradient = new LinearGradient(mWidth / 2 - getTextWidth(mParticleText, mCirclePaint) / 2f, + mHeight / 2 - getTextHeight(mParticleText, mCirclePaint) / 2, + mWidth / 2 - getTextWidth(mParticleText, mCirclePaint) / 2, + mHeight / 2 + getTextHeight(mParticleText, mCirclePaint) / 2, + new int[]{mParticleColor, Color.argb(120, getR(mParticleColor), getG(mParticleColor), getB(mParticleColor))}, null, Shader.TileMode.CLAMP); + mCirclePaint.setShader(linearGradient); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mStatus == STATUS_PARTICLE_GATHER) { + for (int i = 0; i < ROW_NUM; i++) { + for (int j = 0; j < COLUMN_NUM; j++) { + canvas.drawCircle(mParticles[i][j].x, mParticles[i][j].y, mParticles[i][j].radius, mCirclePaint); + } + } + } + + if (mStatus == STATUS_TEXT_MOVING) { + canvas.drawText(mHostText, mHostTextX, mHeight / 2 + getTextHeight(mHostText, mHostBgPaint) / 2, mHostBgPaint); + canvas.drawRect(mHostTextX + mHostRectWidth, mHeight / 2 - getTextHeight(mHostText, mHostBgPaint) / 1.2f, mHostTextX + getTextWidth(mHostText, mHostTextPaint), mHeight / 2 + getTextHeight(mHostText, mHostBgPaint) / 1.2f, mHostTextPaint); + } + + if (mStatus == STATUS_PARTICLE_GATHER) { + canvas.drawRoundRect(new RectF(mWidth / 2 - mSpreadWidth, mStartMinP.y, mWidth / 2 + mSpreadWidth, mEndMinP.y), dip2px(2), dip2px(2), mHostBgPaint); + canvas.drawText(mParticleText, mWidth / 2 - getTextWidth(mParticleText, mParticleTextPaint) / 2, mStartMinP.y + (mEndMinP.y - mStartMinP.y) / 2 + getTextHeight(mParticleText, mParticleTextPaint) / 2, mParticleTextPaint); + } else if (mStatus == STATUS_TEXT_MOVING) { + canvas.drawRoundRect(new RectF(mParticleTextX - dip2px(4), mStartMinP.y, mParticleTextX + getTextWidth(mParticleText, mParticleTextPaint) + dip2px(4), mEndMinP.y), dip2px(2), dip2px(2), mHostBgPaint); + canvas.drawText(mParticleText, mParticleTextX, mStartMinP.y + (mEndMinP.y - mStartMinP.y) / 2 + getTextHeight(mParticleText, mParticleTextPaint) / 2, mParticleTextPaint); + } + + } + + private void startParticleAnim() { + + mStatus = STATUS_PARTICLE_GATHER; + + Collection animList = new ArrayList<>(); + + ValueAnimator textAnim = ValueAnimator.ofInt(DEFAULT_MAX_TEXT_SIZE, mParticleTextSize); + textAnim.setDuration((int) (mTextAnimTime * 0.8f)); + textAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + int textSize = (int) valueAnimator.getAnimatedValue(); + mParticleTextPaint.setTextSize(textSize); + } + }); + animList.add(textAnim); + + for (int i = 0; i < ROW_NUM; i++) { + for (int j = 0; j < COLUMN_NUM; j++) { + final int tempI = i; + final int tempJ = j; + ValueAnimator animator = ValueAnimator.ofObject(new LineEvaluator(), mParticles[i][j], mMinParticles[i][j]); + animator.setDuration(mTextAnimTime + ((int) (mTextAnimTime * 0.02f)) * i + ((int) (mTextAnimTime * 0.03f)) * j); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mParticles[tempI][tempJ] = (Particle) animation.getAnimatedValue(); + if (tempI == ROW_NUM - 1 && tempJ == COLUMN_NUM - 1) { + invalidate(); + } + } + }); + animList.add(animator); + } + } + + AnimatorSet set = new AnimatorSet(); + set.playTogether(animList); + set.start(); + + set.addListener(new AnimListener() { + @Override + public void onAnimationEnd(Animator animation) { + startSpreadAnim(); + } + }); + + } + + private void startSpreadAnim() { + ValueAnimator animator = ValueAnimator.ofFloat(0, getTextWidth(mParticleText, mParticleTextPaint) / 2 + dip2px(4)); + animator.setDuration(mSpreadAnimTime); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mSpreadWidth = (float) animation.getAnimatedValue(); + invalidate(); + } + }); + animator.addListener(new AnimListener() { + @Override + public void onAnimationEnd(Animator animation) { + startHostTextAnim(); + } + }); + animator.start(); + } + + private void startHostTextAnim() { + mStatus = STATUS_TEXT_MOVING; + + Collection animList = new ArrayList<>(); + + ValueAnimator particleTextXAnim = ValueAnimator.ofFloat(mStartMinP.x + dip2px(4), mWidth / 2 - (getTextWidth(mHostText, mHostTextPaint) + getTextWidth(mParticleText, mParticleTextPaint)) / 2 + getTextWidth(mHostText, mHostTextPaint)); + particleTextXAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mParticleTextX = (float) animation.getAnimatedValue(); + } + }); + animList.add(particleTextXAnim); + + ValueAnimator animator = ValueAnimator.ofFloat(0, getTextWidth(mHostText, mHostTextPaint)); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mHostRectWidth = (float) animation.getAnimatedValue(); + } + }); + animList.add(animator); + + ValueAnimator hostTextXAnim = ValueAnimator.ofFloat(mStartMinP.x, mWidth / 2 - (getTextWidth(mHostText, mHostTextPaint) + getTextWidth(mParticleText, mParticleTextPaint) + dip2px(20)) / 2); + hostTextXAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mHostTextX = (float) animation.getAnimatedValue(); + invalidate(); + } + }); + animList.add(hostTextXAnim); + + AnimatorSet set = new AnimatorSet(); + set.playTogether(animList); + set.setDuration(mHostTextAnimTime); + set.addListener(new AnimListener() { + @Override + public void onAnimationEnd(Animator animation) { + if (null != mParticleAnimListener) { + mParticleAnimListener.onAnimationEnd(); + } + } + }); + set.start(); + + } + + public void startAnim() { + post(new Runnable() { + @Override + public void run() { + startParticleAnim(); + } + }); + } + + private abstract class AnimListener implements Animator.AnimatorListener { + @Override + public void onAnimationStart(Animator animation) { + + } + + @Override + public void onAnimationCancel(Animator animation) { + + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + } + + public void setOnParticleAnimListener(ParticleAnimListener particleAnimListener) { + mParticleAnimListener = particleAnimListener; + } + + public interface ParticleAnimListener { + void onAnimationEnd(); + } + + private int dip2px(float dipValue) { + final float scale = getContext().getResources().getDisplayMetrics().density; + return (int) (dipValue * scale + 0.5f); + } + + private int sp2px(float spValue) { + final float fontScale = getContext().getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } + + private float getTextHeight(String text, Paint paint) { + Rect rect = new Rect(); + paint.getTextBounds(text, 0, text.length(), rect); + return rect.height() / 1.1f; + } + + private float getTextWidth(String text, Paint paint) { + return paint.measureText(text); + } + + private int getR(int color) { + int r = (color >> 16) & 0xFF; + return r; + } + + private int getG(int color) { + int g = (color >> 8) & 0xFF; + return g; + } + + private int getB(int color) { + int b = color & 0xFF; + return b; + } +} + diff --git a/widgets/src/main/res/color/mdtp_date_picker_selector.xml b/widgets/src/main/res/color/mdtp_date_picker_selector.xml new file mode 100644 index 0000000..15606d2 --- /dev/null +++ b/widgets/src/main/res/color/mdtp_date_picker_selector.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/widgets/src/main/res/color/mdtp_date_picker_year_selector.xml b/widgets/src/main/res/color/mdtp_date_picker_year_selector.xml new file mode 100644 index 0000000..eb2ce13 --- /dev/null +++ b/widgets/src/main/res/color/mdtp_date_picker_year_selector.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/widgets/src/main/res/color/mdtp_done_text_color.xml b/widgets/src/main/res/color/mdtp_done_text_color.xml new file mode 100644 index 0000000..7e99f97 --- /dev/null +++ b/widgets/src/main/res/color/mdtp_done_text_color.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/widgets/src/main/res/color/mdtp_done_text_color_dark.xml b/widgets/src/main/res/color/mdtp_done_text_color_dark.xml new file mode 100644 index 0000000..1c16f23 --- /dev/null +++ b/widgets/src/main/res/color/mdtp_done_text_color_dark.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/widgets/src/main/res/drawable/mdtp_done_background_color.xml b/widgets/src/main/res/drawable/mdtp_done_background_color.xml new file mode 100644 index 0000000..f301fcc --- /dev/null +++ b/widgets/src/main/res/drawable/mdtp_done_background_color.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/widgets/src/main/res/drawable/mdtp_done_background_color_dark.xml b/widgets/src/main/res/drawable/mdtp_done_background_color_dark.xml new file mode 100644 index 0000000..d494c14 --- /dev/null +++ b/widgets/src/main/res/drawable/mdtp_done_background_color_dark.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/widgets/src/main/res/drawable/mdtp_material_button_background.xml b/widgets/src/main/res/drawable/mdtp_material_button_background.xml new file mode 100644 index 0000000..f70c39c --- /dev/null +++ b/widgets/src/main/res/drawable/mdtp_material_button_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/widgets/src/main/res/drawable/mdtp_material_button_selected.xml b/widgets/src/main/res/drawable/mdtp_material_button_selected.xml new file mode 100644 index 0000000..1733e2d --- /dev/null +++ b/widgets/src/main/res/drawable/mdtp_material_button_selected.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/widgets/src/main/res/layout/mdtp_date_picker_dialog.xml b/widgets/src/main/res/layout/mdtp_date_picker_dialog.xml new file mode 100644 index 0000000..04acb71 --- /dev/null +++ b/widgets/src/main/res/layout/mdtp_date_picker_dialog.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widgets/src/main/res/layout/mdtp_date_picker_header_view.xml b/widgets/src/main/res/layout/mdtp_date_picker_header_view.xml new file mode 100644 index 0000000..517ecf7 --- /dev/null +++ b/widgets/src/main/res/layout/mdtp_date_picker_header_view.xml @@ -0,0 +1,26 @@ + + + diff --git a/widgets/src/main/res/layout/mdtp_date_picker_selected_date.xml b/widgets/src/main/res/layout/mdtp_date_picker_selected_date.xml new file mode 100644 index 0000000..c39a76b --- /dev/null +++ b/widgets/src/main/res/layout/mdtp_date_picker_selected_date.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widgets/src/main/res/layout/mdtp_date_picker_view_animator.xml b/widgets/src/main/res/layout/mdtp_date_picker_view_animator.xml new file mode 100644 index 0000000..458d865 --- /dev/null +++ b/widgets/src/main/res/layout/mdtp_date_picker_view_animator.xml @@ -0,0 +1,21 @@ + + \ No newline at end of file diff --git a/widgets/src/main/res/layout/mdtp_done_button.xml b/widgets/src/main/res/layout/mdtp_done_button.xml new file mode 100644 index 0000000..c1582cd --- /dev/null +++ b/widgets/src/main/res/layout/mdtp_done_button.xml @@ -0,0 +1,41 @@ + + + + + + + diff --git a/widgets/src/main/res/layout/mdtp_time_header_label.xml b/widgets/src/main/res/layout/mdtp_time_header_label.xml new file mode 100644 index 0000000..efa46c9 --- /dev/null +++ b/widgets/src/main/res/layout/mdtp_time_header_label.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + diff --git a/widgets/src/main/res/layout/mdtp_time_picker_dialog.xml b/widgets/src/main/res/layout/mdtp_time_picker_dialog.xml new file mode 100644 index 0000000..826ef7c --- /dev/null +++ b/widgets/src/main/res/layout/mdtp_time_picker_dialog.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + diff --git a/widgets/src/main/res/layout/mdtp_year_label_text_view.xml b/widgets/src/main/res/layout/mdtp_year_label_text_view.xml new file mode 100644 index 0000000..f0bd077 --- /dev/null +++ b/widgets/src/main/res/layout/mdtp_year_label_text_view.xml @@ -0,0 +1,23 @@ + + + diff --git a/widgets/src/main/res/values-v21/styles.xml b/widgets/src/main/res/values-v21/styles.xml new file mode 100644 index 0000000..8405313 --- /dev/null +++ b/widgets/src/main/res/values-v21/styles.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/widgets/src/main/res/values/attrs.xml b/widgets/src/main/res/values/attrs.xml new file mode 100644 index 0000000..286873e --- /dev/null +++ b/widgets/src/main/res/values/attrs.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widgets/src/main/res/values/colors.xml b/widgets/src/main/res/values/colors.xml new file mode 100644 index 0000000..2c377a8 --- /dev/null +++ b/widgets/src/main/res/values/colors.xml @@ -0,0 +1,76 @@ + + + + #9C27B0 + #9C27B0 + #a828be + #FF4081 + #cf33ea + #111 + #fff + #0f6afc + #f20a0a + #09f415 + #f5b504 + #eded80 + #b7afaf + #662e03 + #FFD700 + #FF565656 + #484848 + #235BD4 + #2EA4E4 + + + ///////////////Date Picker + + @color/YellowLight + @color/YellowLight + @color/YellowLight + @color/YellowLight + #cccccc + @color/mdtp_numbers_text_color + #ff212121 + #cccccc + #ff212121 + + #7f000000 + #76ffffff + #ff212121 + @color/mdtp_date_picker_text_normal + #ccc + #767676 + + + @color/BlackColor + @color/mdtp_white + #b4b4b4 + @color/WhiteColor + @color/mdtp_date_picker_text_normal_dark_theme + @color/colorAccent + @color/colorPrimaryDark + + @android:color/white + + @color/mdtp_accent_color + #33969696 + + + #525252 + + + @color/mdtp_accent_color + @color/mdtp_accent_color_dark + #424242 + #323232 + #808080 + #ffffff + #888888 + + + #444444 + #888888 + #888888 + + + diff --git a/widgets/src/main/res/values/dimens.xml b/widgets/src/main/res/values/dimens.xml new file mode 100644 index 0000000..3e65775 --- /dev/null +++ b/widgets/src/main/res/values/dimens.xml @@ -0,0 +1,61 @@ + + + 16dp + 16dp + 16dp + 16dp + 8dp + 12dp + 65dp + 5dp + + + + + ///////DatePicker + 0.82 + 0.85 + 0.16 + 0.22 + 0.81 + 0.60 + 0.83 + 0.12 + 0.11 + 0.08 + + 60sp + -30dp + 16sp + 6dip + 4dip + 96dip + 270dip + 300dip + + 270dp + 155dp + 190dp + 252dp + + 56dp + 12sp + 16dp + 45dp + 20dp + 50dp + 25dp + 12sp + 14sp + 12sp + 64dp + 22dp + + 48dp + 14sp + 64dp + 8dp + + 14sp + + diff --git a/widgets/src/main/res/values/strings.xml b/widgets/src/main/res/values/strings.xml new file mode 100644 index 0000000..b9103c5 --- /dev/null +++ b/widgets/src/main/res/values/strings.xml @@ -0,0 +1,713 @@ + + CustomWidgets + + تایید + بیخیال + + "لغزنده دایره‌ای ساعت" + + "لغزنده دایره‌ای دقیقه" + + "انتخاب ساعت" + + "انتخاب دقیقه" + + + Month grid of days + + Year list + + Select month and day + + Select year + + "%1$s انتخاب شد" + + "%1$s حذف شد" + + + -- + + : + + + sans-serif + + sans-serif + + + sans-serif + + + "https://bookdocs.ir/" + "https://bookdocs.ir/Doctors/public/images/patients/" + "https://bookdocs.ir/Doctors/public/images/doctors/" + + + جمعیت شناسی + جهانگردی + حسابداری + حسابداری مالیاتی + حشره شناسی كشاورزی + حفاظت اطلاعات + حفاظت و مرمت آثار تاریخی + حقوق + حقوق بین الملل عمومی + حقوق جزا و جرم شناسی + حقوق خصوصی + حقوق عمومی + حقوق قضایی + حقوق نفت و گاز + حمل و نقل ریلی + خط و سازه های ریلی + داروسازی + دامپزشكی + دریا + دریا ـ دریانوردی + دندانپزشكی + رادیولوژی دامپزشكی + راه آهن + روابط عمومی + روابط كار + روان شناسی اجتماعی + روان شناسی تربیتی + روان شناسی و آموزش كودكان استثنایی + روانشناسی + روزنامه نگاری + روزنامه نگاری علوم ارتباطات + ریاضی كاربردی - آنالیز عددی + ریاضی كاربردی - زیست ریاضی + ریاضی محض + ریاضیات و كاربردها + ریز زیست فناوری + زبان اسپانیایی + زبان ایتالیایی + زبان روسی + زبان شناسی + زبان و ادبیات آلمانی + زبان و ادبیات اردو + زبان و ادبیات ارمنی + زبان و ادبیات اسپانیایی + زبان و ادبیات انگلیسی + زبان و ادبیات تركی آذری + زبان و ادبیات تركی استانبولی + زبان و ادبیات عربی + زبان و ادبیات فارسی + زبان و ادبیات فرانسه + زبان و ادبیات كردی + زبان و ادبیات ژاپنی + زبان چینی + زبانهای باستانی ایران + زمین شناسی + زمین شناسی آب شناسی + زمین شناسی اقتصادی + زمین شناسی تكتونیك + زمین شناسی زیست محیطی + زمین شناسی سنگ شناسی رسوبی + زمین شناسی فسیل شناسی و چینه شناسی + زمین شناسی مهندسی + زمین شناسی نفت + زمین شناسی پترولوژی + زیست شناسی + زیست شناسی جانوری + زیست شناسی دریا + زیست شناسی سلولی و مولكولی + زیست شناسی ـ سلولی و تكوینی گیاهی + زیست شناسی ـ سلولی و ملكولی + زیست شناسی ـ سیستماتیك و بوم شناسی گیاهی + زیست شناسی ـ فیزیولوژی گیاهی + زیست شناسی ـ میكروبیولوژی + زیست شناسی ـ ژنتیك مولكولی + زیست شناسی محاسباتی بیوفیزیك + زیست شناسی گیاهی + زیست فناوری + زیست فناوری میكروبی + ساخت و تولید + سم شناسی + سنجش از دور و سامانه اطلاعات جغرافیایی + سنجش و اندازه گیری + سینما + شناسایی و مبارزه با علفهای هرز + شنوایی شناسی + شهرسازی + شهرسازی - انرژی شهرسازی + شیعه شناسی + شیلات ـ تولید و بهره برداری + شیلات ـ عمل آوری + شیمی + شیمی آزمایشگاهی + شیمی ـ شیمی آلی + شیمی ـ شیمی فیزیك + شیمی ـ شیمی معدنی + شیمی ـ شیمی پلیمر + شیمی كاربردی + شیمی محض + صنایع + صنایع خمیر و كاغذ + صنایع دستی + صنایع مبلمان + صنایع چوب و فرآورده های سلولزی + ضد تروریسم + طبیعت + طراحی شهری + طراحی صحنه + طراحی صنعتی + طراحی لباس + طراحی پارچه + طراحی پارچه و لباس + عكاسی + علم اطلاعات و دانش شناسی + علوم آزمایشگاهی + علوم آزمایشگاهی دامپزشكی + علوم ارتباطات اجتماعی + علوم اطلاعاتی + علوم اقتصادی + علوم انتظامی + علوم تربیتی + علوم تغذیه + علوم جانوری ـ بیوسیستماتیك جانوری + علوم جانوری ـ فیزیولوژی جانوری + علوم جنگل ـ جنگل شناسی + علوم جنگل ـ جنگل داری و مسائل اقتصادی + علوم جنگل ـ مهندسی جنگل + علوم دامی + علوم زمین + علوم سیاسی + علوم شناختی + علوم علف های هرز + علوم فنی امنیت + علوم قرآن و حدیث + علوم قضایی + علوم كامپیوتر + علوم محیط زیست + علوم مهندسی + علوم و آب + علوم و باغبانی + علوم و جنگل + علوم و خاك + علوم و شیلات + علوم و صنایع غذایی + علوم و صنایع غذایی كشاورزی) + علوم و فناوری نانو ـ نانوشیمی + علوم و فناوری نانو - نانوفیزیك + علوم و محیط زیست + علوم و مهندسی آب + علوم و مهندسی آب ـ آبیاری و زهكشی + علوم و مهندسی آب ـ سازه های آبی + علوم و مهندسی آب ـ منابع آب + علوم و مهندسی آب ـ هوا شناسی كشاورزی + علوم و مهندسی آبخیزداری + علوم و مهندسی باغبانی + علوم و مهندسی جنگل + علوم و مهندسی شیلات + علوم و مهندسی صنایع غذایی + علوم و مهندسی محیط زیست + علوم و مهندسی مرتع + علوم ورزشی + عمران + فارماكولوژی + فرش + فرهنگ و زبانهای باستانی + فرهنگ و معارف اسلامی + فضای سبز + فقه شافعی + فقه و حقوق اسلامی + فقه و حقوق امامی + فقه و حقوق حنفی + فقه و حقوق شافعی + فقه و مبانی حقوق اسلامی + فلسفه + فلسفه تعلیم و تربیت + فلسفه علم + فلسفه منطق + فلسفه و حكمت اسلامی + فلسفه و عرفان اسلامی + فلسفه و كلام اسلامی + فناوری اطلاعات سلامت + فناوری اطلاعات و ارتباطات + جوشكاری + عملیات پتروشیمی + فناوری تولید مثل در دامپزشكی + فناوری نانو ـ نانوالكتریك + فناوری نانو ـ نانومواد + فوتونیك + فوریتهای پزشكی + فیتوشیمی + فیزیك + فیزیك دریا + فیزیك مهندسی + فیزیوتراپی + فیزیولوژی + فیزیولوژی دامپزشكی + فیزیولوژی علوم جانوری ـ تكوینی + قارچ شناسی + قارچ شناسی دامپزشكی + كار آفرینی + كاردرمانی + كارگردانی تلویزیون + كامپیوتر + كتابت و نگارگری + كتابداری در شاخه پزشكی + كلینیكال پاتولوژی دامپزشكی + ماشین آلات دریایی + ماشینهای ریلی + ماشینهای صنایع غذایی + مامایی + مامایی و بیماریهای تولید مثل دام + مترجمی زبان آلمانی + مترجمی زبان انگلیسی + مترجمی زبان عربی + مترجمی زبان فرانسه + مجسمه سازی + برنامه ریزی شهری + مدیریت شهری + تربیت بدنی و علوم ورزشی + ریاضی + زبان آلمانی + زبان انگلیسی + زبان عربی + زبان فرانسه + زراعت و اصلاح نباتات + شیمی ـ بیوتكنولوژی و داروسازی + علوم اجتماعی + علوم جغرافیایی + علوم دام و طیور + علوم سیاسی و روابط بین الملل + فتونیك + مدیریت + مدیریت حاصلخیزی، زیست فناوری و منابع خاك + مدیریت كسب و كار و امور شهری + معماری + مهندسی برق + مهندسی شیمی + مهندسی عمران + مهندسی معماری كشتی + مهندسی مكانیك + مهندسی مواد و متالورژی + مهندسی هوافضا + هنرهای ساخت و معماری + هنرهای موسیقی + هنرهای نمایشی و سینما + هنرهای پژوهشی و صنایع دستی + ژئوفیزیك و هواشناسی + محیط زیست + محیط زیست ـ برنامه ریزی + مخابرات + مخابرات هواپیمایی + مددكاری اجتماعی + مدرسی معارف اسلامی + مدیریت اطلاعاتی + مدیریت امور بانكی + مدیریت امور گمركی + مدیریت بازرگانی + مدیریت بیمه + مدیریت بیمه اكو + مدیریت جهانگردی + مدیریت خدمات اجتماعی مددكاری اجتماعی + مدیریت خدمات بهداشتی درمانی + مدیریت دریایی + مدیریت دولتی + مدیریت راهبردی و آینده پژوهی + مدیریت صنعتی + مدیریت فرهنگی هنری + مدیریت فناوری اطلاعات + مدیریت قراردادهای بین المللی نفت و گاز + مدیریت كسب و كارهای كوچك + مدیریت كشاورزی + مدیریت مالی + مدیریت منابع خاك + مدیریت هتل داری گردشگری + مدیریت و بازرگانی دریایی + مدیریت و كنترل بیابان + مدیریت پروژه + مربیگری عقیدتی + مربیگری ورزشی + مردم شناسی + مرمت آثار تاریخی + مرمت بناهای تاریخی + مرمت و احیای ابنیه و بافتهای تاریخی + مشاوره + مطالعات ارتباطی و فناوری اطلاعات + مطالعات تعاونی های علوم اجتماعی + مطالعات جهان + مطالعات خانواده + مطالعات زنان + معارف اسلامی + معارف اسلامی ـ تبلیغ و ارتباطات + معارف اسلامی و ارشاد + معارف اسلامی و تاریخ + معارف اسلامی و حقوق + معارف اسلامی و علوم تربیتی + معارف اسلامی و كلام + معارف اسلامی و مدیریت + معدن + معماری داخلی + معماری سنتی + مكانیزاسیون كشاورزی + مكانیك + مكانیك بیوسیستم كشاورزی) + مهندسی ابزار دقیق و اتوماسیون در صنایع نفت + مهندسی اقتصاد كشاورزی + مهندسی ایمنی و بازرسی فنی + مهندسی برق ـ الكترونیك + مهندسی برق ـ قدرت + مهندسی برق ـ كنترل + مهندسی برق ـ مخابرات + مهندسی در سوانح طبیعی + مهندسی دریا + مهندسی سیستمهای انرژی + مهندسی شیمی ـ بهداشت، ایمنی و محیط زیست HSE) + مهندسی شیمی ـ بیوتكنولوژی + مهندسی شیمی ـ بیوتكنولوژی و داروسازی + مهندسی صنایع + مهندسی صنایع چوب و فرآورده های سلولزی + مهندسی طراحی محیط زیست + مهندسی عمران ـ حمل و نقل + مهندسی عمران ـ راه و ترابری + مهندسی عمران ـ زلزله + مهندسی عمران ـ سازه + مهندسی عمران ـ سواحل، بنادر و سازههای دریایی + مهندسی عمران ـ محیط زیست + مهندسی عمران ـ مدیریت ساخت + مهندسی عمران ـ مدیریت منابع آب + مهندسی عمران ـ مهندسی آب و سازه های هیدرولیكی + مهندسی عمران ـ ژئوتكنیك + مهندسی فضای سبز + مهندسی فناوری اطلاعات IT) + مهندسی كامپیوتر + مهندسی كامپیوتر ـ شبكه و رایانش + مهندسی كامپیوتر ـ معماری سیستم های كامپیوتری + مهندسی كامپیوتر ـ نرم افزار و الگوریتم + مهندسی كامپیوتر ـ هوش مصنوعی + مهندسی متابولیك - زراعت + مهندسی محیط زیست ـ آب و فاضلاب + مهندسی محیط زیست ـ آلودگی هوا + مهندسی محیط زیست ـ منابع آب + مهندسی محیط زیست ـ مواد زائد جامد + مهندسی معدن + مهندسی معدن ـ استخراج + مهندسی معدن ـ اكتشاف + مهندسی معدن ـ فرآوری مواد معدنی + مهندسی معدن ـ مكانیك سنگ + مهندسی مكانیزاسیون كشاورزی + مهندسی مكانیك بیوسیستم + مهندسی مكانیك ـ تبدیل انرژی + مهندسی مكانیك ـ دینامیك، كنترل و ارتعاشات + مهندسی مكانیك ـ ساخت و تولید + مهندسی مكانیك ـ مكانیك جامدات + مهندسی نساجی + مهندسی نساجی ـ تكنولوژی نساجی + مهندسی نساجی ـ شیمی نساجی و علوم الیاف + مهندسی نفت + مهندسی نفت - حفاری + مهندسی نفت ـ اكتشاف + مهندسی نقشه برداری + مهندسی نقشه برداری ـ سنجش از دور + مهندسی نقشه برداری ـ سیستم اطلاعات جغرافیایی GIS) + مهندسی نقشه برداری ـ فتوگرامتری + مهندسی نقشه برداری ـ ژئودزی + مهندسی هسته ای ـ راكتور + مهندسی هسته ای ـ كاربرد پرتوها + مهندسی هسته ای ـ پرتو پزشكی + مهندسی هسته ای ـ گداخت + مهندسی هوا فضا ـ آئرودینامیك + مهندسی هوا فضا ـ جلوبرندگی + مهندسی هوا فضا ـ دینامیك پرواز و كنترل + مهندسی هوا فضا ـ سازه های هوایی + مهندسی پزشكی ـ بیوالكتریك + مهندسی پزشكی ـ بیومتریال + مهندسی پزشكی ـ بیومكانیك + مهندسی پلیمر + مهندسی پلیمر ـ رنگ + مهندسی پلیمر ـ صنایع رنگ + مهندسی پلیمر ـ پلیمر + مواد و متالورژی + موزه + موسیقی نظامی + میكروبیولوژی + نانوفناوری ـ نانومواد + نساجی + نفت + نقاشی + نقشه برداری + نمایش عروسكی + نمونه گیری و آمار + نوازندگی موسیقی ایرانی + نوازندگی موسیقی جهانی + هتل داری + هنر اسلامی + هنر سفالگری + هنرهای تجسمی + هنرهای صناعی + هنرهای چند رسانه ای + هواشناسی + هوافضا + هوانوردی + هوشبری + ویروس شناسی + یادگیری فناورانه تكنولوژی آموزشی + پاتولوژی دامپزشكی + پالایش گاز + پرستاری + پزشكی + پلیمر + پژوهش هنر + پژوهشگری اجتماعی + پژوهشگری امنیت + چاپ + ژئوفیزیك ـ الكترومغناطیس + ژئوفیزیك ـ زلزله شناسی + ژئوفیزیك ـ لرزه شناسی + ژئوفیزیك ـ گرانی سنجی + ژئومورفولوژی + ژنتیك و به نژادی گیاهی + گرافیك + گفتار درمانی + گیاه پزشكی + آب و هواشناسی + آمار + آمار و سنجش آموزشی + آموزش ابتدایی + آموزش الهیات و معارف اسلامی + آموزش تاریخ + آموزش جغرافیا + آموزش راهنمایی و مشاوره + آموزش ریاضی + آموزش زبان آلمانی + آموزش زبان انگلیسی + آموزش زبان روسی + آموزش زبان فرانسه + آموزش زبان و ادبیات عربی + آموزش زبان و ادبیات فارسی + آموزش زیست شناسی + آموزش شیمی + آموزش علوم اجتماعی + آموزش علوم ورزشی + آموزش فیزیك + آموزش كودكان استثنایی + آموزش و پرورش ابتدایی + آناتومی و جنین شناسی + آهنگسازی + اتاق عمل + بهداشت و كنترل كیفی مواد غذایی + ادبیات نمایشی + ادیان و عرفان + ادیان و مذاهب + ارتباط تصویری + ارتباطات و فناوری اطلاعات + اشتغال + اعضای مصنوعی + اقتصاد + اقتصاد اسلامی + اقتصاد كار و بهره وری + اقتصاد كشاورزی + اقتصاد نفت و گاز + اقیانوس شناسی + اقیانوس شناسی فیزیكی + اكوهیدرولوژی + الكترونیك هواپیمایی + الكترونیك و مخابرات دریایی + الهیات ـ ادیان و عرفان + الهیات ـ تاریخ و تمدن ملل اسلامی + الهیات ـ علوم قرآن و حدیث + الهیات ـ فقه و مبانی حقوق اسلامی + الهیات ـ كلام + الهیات و معارف اسلامی فقه شافعی + امنیت اطلاعات + امنیت اقتصادی + امنیت بین الملل + امنیت نرم + امور اراضی + امور بانكی + امور تربیتی + امور دولتی + امور زراعی و باغی + امور فرهنگی + امور مالی و مالیاتی + انرژی + انیمیشن + انگل شناسی + انگل شناسی دامپزشكی + ایران شناسی + ایمنی + ایمنی شناسی + ایمنی شناسی دامپزشكی + ایمنی صنعتی + اپتیك و لیزر + اپیدمیولوژی + بازیگری + باستان سنجی شیمی ـ شیمی تجزیه + باستان شناسی + بافت شناسی + بافت شناسی دامپزشكی + باكتری شناسی + باكتری شناسی دامپزشكی + برق + برنامه ریزی آموزشی مدیریت آموزشی + برنامه ریزی اجتماعی و تعاون + برنامه ریزی درسی + بهداشت حرفه ای + بهداشت خوراك دام + بهداشت عمومی + بهداشت محیط + بهداشت مواد غذایی + بهداشت و بازرسی گوشت + بهداشت و بیماریهای آبزیان + بهداشت و بیماریهای پرندگان + بهره برداری راه اهن + بهینه سازی مصرف انرژی + بوم شناسی زراعی + بیماری شناسی گیاهی + بیمه + بینایی سنجی + بیهوشی و مراقبتهای ویژه دامپزشكی + بیوانفورماتیك + بیوتكنولوژی + بیوتكنولوژی كشاورزی + بیوشیمی + بیوشیمی بالینی + بیولوژی و آناتومی چوب + بیولوژی و كنترل ناقلین بیماریها + تاریخ + تاریخ اسلام + تاریخ ـ تاریخ اسلام + تاریخ ـ تاریخ ایران دوره اسلام + تاریخ ـ تاریخ ایران قبل ازاسلام + تاریخ و تمدن ملل اسلامی + تاریخ و فلسفه علم + تربیت بدنی ـ آسیب شناسی ورزشی + تربیت بدنی ـ بیومكانیك ورزشی + تربیت بدنی ـ رفتارحركتی + تربیت بدنی ـ فیزیولوژی ورزشی + تربیت بدنی ـ مدیریت ورزشی + تربیت مبلغ قرآن كریم + تربیت مروج سیاسی + تربیت معلم قرآن كریم + ترجمه + ترویج و آموزش كشاورزی پایدار + تعمیر و نگهداری هواپیما + تكثیر و پرورش آبزیان + تكنسین سلامت دهان + تكنسین پروتزهای دندانی + تكنولوژی آبیاری + تكنولوژی تولیدات دامی + تكنولوژی تولیدات گیاهی + تكنولوژی رادیولوژی دهان، فك و صورت + تكنولوژی صنایع غذایی + تكنولوژی ماشینهای كشاورزی + تكنولوژی محیط زیست + تكنولوژی مرتع و آبخیزداری + تكنولوژی مواد غذایی + تكنولوژی پرتو درمانی + تكنولوژی پرتوشناسی + تكنولوژی پزشكی هسته ای + تلویزیون و هنرهای دیجیتالی + توسعه روستایی + توسعه كشاورزی + تولید و بهره برداری ازگیاهان دارویی و معطر + تولید و فرآوری خرما + تولید و ژنتیك گیاهی + جامعه شناسی + جراحی دامپزشكی + جغرافیا + جغرافیا و برنامه ریزی روستایی + جغرافیا و برنامه ریزی شهری + جغرافیای سیاسی + علوم انسانی + علوم تجربی + ریاضی و فیزیک + + + + پزشک + کلینیک + داروخانه + آزمایشگاه + بیمارستان + فیزیوتراپی + تجهیزات پزشکی + + + + doctor + clinik + pharmacy + lab + hospital + physiotherapy + medical_equipment + + + + select_doctorsAdver + select_places + select_lab_hospitals + select_lab_hospitals + select_lab_hospitals + select_lab_hospitals + select_lab_hospitals + + + + 1//پزشک + 2//کلینیک + 1//داروخانه + 2آزمایشگاه// + 3//بیمارستان + 4//فیزیوتراپی + 5//تجحیزات پزشکی + + + + شرکت شبکه رفاه ایرانیان (سهامی خاص) در سال 1395 با هدف خدمت رسانی به شما هموطنان عزیز و ایجاد فضای مناسب برای کاریابی و اشتغال زایی راه اندازی گردید. + +
    +
    + اهداف و سیاست های کلی رفاه من +
    + + ارائه مرجعی کامل از اماکن تفریحی، ورزشی و گردشگری تخفیف دار در سطح کشور +
    + تسهیل در نوبت دهی غیر حضوری و آنلاین پزشکان متخصص و درمانگاه ها +
    + ارائه مرجع کامل و تخصصی کسب و کار و کاریابی +
    + فراهم نمودن بستری مناسب جهت بیشتر دیده شدن کسب و کار و فعالان اقتصادی +
    + ایجاد شرایط مطلوب جهت اشتغال زایی برای عزیزانی ک تابحال سابقه کار نداشته اند. +
    + توزیع یکنواخت کار بین متخصصین مشاغل +
    + قرار دادن افراد در تخصص های مرتبط با تجربه کاری و ایجاد بستر مناسب برای رشد و توسعه فردی +
    + فراهم نمودن فضای مناسب درجهت سرعت بخشیدن به امر استخدام و کاریابی در سالی که به نام " اقتصاد مقاومتی ، تولید و اشتغال " مزین گردیده است. +
    +

    + ]]> +
    + + + + چرخش + معکوس کردن + + انتخاب فایل + +
    diff --git a/widgets/src/main/res/values/styles.xml b/widgets/src/main/res/values/styles.xml new file mode 100644 index 0000000..9fe576b --- /dev/null +++ b/widgets/src/main/res/values/styles.xml @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + //DatePicker + + + + + + + + + + + + diff --git a/widgets/src/test/java/mohammadaminha/com/widgets/ExampleUnitTest.java b/widgets/src/test/java/mohammadaminha/com/widgets/ExampleUnitTest.java new file mode 100644 index 0000000..30ef48d --- /dev/null +++ b/widgets/src/test/java/mohammadaminha/com/widgets/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package mohammadaminha.com.widgets; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file