├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── build.gradle ├── example ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── solar │ │ └── blaz │ │ └── date │ │ └── week │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── solar │ │ └── blaz │ │ └── date │ │ └── week │ │ └── MainActivity.java │ └── res │ ├── color │ └── date_picker_text_color.xml │ ├── drawable │ ├── date_picker_day_bg.xml │ └── date_picker_indicator.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── example_screenshot.gif └── example_screenshot.png ├── library ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── solar │ │ └── blaz │ │ └── date │ │ └── week │ │ └── WeekDatePicker.java │ └── res │ └── values │ └── attrs.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | #!! ERROR: andr is undefined. Use list command to see defined gitignore types !!# 4 | 5 | ### Android ### 6 | # Built application files 7 | *.apk 8 | *.ap_ 9 | 10 | # Files for the Dalvik VM 11 | *.dex 12 | 13 | # Java class files 14 | *.class 15 | 16 | # Generated files 17 | bin/ 18 | gen/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | 34 | ### Crashlytics ### 35 | #Crashlytics 36 | crashlytics-build.properties 37 | com_crashlytics_export_strings.xml 38 | 39 | 40 | ### Intellij ### 41 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 42 | 43 | *.iml 44 | 45 | ## Directory-based project format: 46 | .idea/ 47 | # if you remove the above rule, at least ignore the following: 48 | 49 | # User-specific stuff: 50 | # .idea/workspace.xml 51 | # .idea/tasks.xml 52 | # .idea/misc.xml 53 | # .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | # .idea/dataSources.ids 57 | # .idea/dataSources.xml 58 | # .idea/sqlDataSources.xml 59 | # .idea/dynamic.xml 60 | # .idea/uiDesigner.xml 61 | 62 | # Gradle: 63 | # .idea/gradle.xml 64 | # .idea/libraries 65 | 66 | ## File-based project format: 67 | *.ipr 68 | *.iws 69 | 70 | ## Plugin-specific files: 71 | 72 | # IntelliJ 73 | out/ 74 | 75 | # mpeltonen/sbt-idea plugin 76 | .idea_modules/ 77 | 78 | # JIRA plugin 79 | atlassian-ide-plugin.xml 80 | 81 | # Crashlytics plugin (for Android Studio and IntelliJ) 82 | com_crashlytics_export_strings.xml 83 | 84 | 85 | ### OSX ### 86 | .DS_Store 87 | .AppleDouble 88 | .LSOverride 89 | 90 | # Icon must end with two \r 91 | Icon 92 | 93 | 94 | # Thumbnails 95 | ._* 96 | 97 | # Files that might appear on external disk 98 | .Spotlight-V100 99 | .Trashes 100 | 101 | # Directories potentially created on remote AFP share 102 | .AppleDB 103 | .AppleDesktop 104 | Network Trash Folder 105 | Temporary Items 106 | .apdisk 107 | 108 | 109 | ### Windows ### 110 | # Windows image file caches 111 | Thumbs.db 112 | ehthumbs.db 113 | 114 | # Folder config file 115 | Desktop.ini 116 | 117 | # Recycle Bin used on file shares 118 | $RECYCLE.BIN/ 119 | 120 | # Windows Installer files 121 | *.cab 122 | *.msi 123 | *.msm 124 | *.msp 125 | 126 | # Windows shortcuts 127 | *.lnk 128 | 129 | 130 | ### Linux ### 131 | *~ 132 | 133 | # KDE directory preferences 134 | .directory 135 | 136 | 137 | ### Gradle ### 138 | .gradle 139 | build/ 140 | gradle/* 141 | !gradle/wrapper/* 142 | 143 | # Ignore Gradle GUI config 144 | gradle-app.setting 145 | 146 | # Android gradle captures 147 | captures/ 148 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Version 1.2 *(11.6.2016)* 4 | 5 | * Adding date limitation functionality 6 | 7 | ## Version 1.1 *(21.3.2016)* 8 | 9 | * Prefixing attributes. 10 | 11 | ## Version 1.0.0 *(17.2.2016)* 12 | 13 | * Initial release 14 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors to WeekDatePicker project 2 | 3 | ## Creator & Maintainer 4 | 5 | * Blaž Šolar <[blaz.solar](http://blaz.solar)> 6 | 7 | ## Contributors 8 | 9 | In chronological order: 10 | 11 | * [Your name or handle] <[email or website]> 12 | * [Brief summary of your changes] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Blaž Šolar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WeekDatePicker 2 | 3 | WeekDatePicker is a lightweight implementation of week date picker for Android. It is based on a View instead of ViewGroup. That gives it benefit of being memory efficient, fast and adds ability of infinite scrolling. 4 | 5 | Date implementation is based on Java 8 Date Time backport [threeten](http://www.threeten.org/) 6 | 7 | ## Example 8 | ![Example screenshot](images/example_screenshot.gif) 9 | 10 | Source code with examples is included in repository. 11 | 12 | ## Dependencies 13 | ### Gradle 14 | ``` 15 | compile "solar.blaz:week-date-picker:1.2" 16 | ``` 17 | 18 | ## Usage 19 | ```xml 20 | 25 | 26 | 39 | 40 | 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.1.2' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 'Google Inc.:Google APIs:23' 5 | buildToolsVersion "23.0.3" 6 | 7 | defaultConfig { 8 | applicationId "solar.blaz.date.week.example" 9 | minSdkVersion 15 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile 'com.android.support:appcompat-v7:23.4.0' 24 | compile project(':week-date-picker') 25 | 26 | testCompile 'junit:junit:4.12' 27 | } 28 | -------------------------------------------------------------------------------- /example/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/blazsolar/Documents/workspace/android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /example/src/androidTest/java/solar/blaz/date/week/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package solar.blaz.date.week; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/src/main/java/solar/blaz/date/week/MainActivity.java: -------------------------------------------------------------------------------- 1 | package solar.blaz.date.week; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | 6 | import org.threeten.bp.LocalDate; 7 | 8 | import solar.blaz.date.week.example.R; 9 | 10 | /** 11 | * Created by Blaz Solar on 23/01/16. 12 | */ 13 | public class MainActivity extends Activity { 14 | 15 | private WeekDatePicker datePicker; 16 | 17 | @Override protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_main); 20 | 21 | datePicker = (WeekDatePicker) findViewById(R.id.date_picker); 22 | 23 | datePicker.setDateIndicator(LocalDate.now().plusDays(1), true); 24 | datePicker.setLimits(LocalDate.now().minusWeeks(1), null); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/src/main/res/color/date_picker_text_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/date_picker_day_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/date_picker_indicator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazsolar/WeekDatePicker/3595a5570c840282bc0c01e28cd4d80d323098fa/example/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazsolar/WeekDatePicker/3595a5570c840282bc0c01e28cd4d80d323098fa/example/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazsolar/WeekDatePicker/3595a5570c840282bc0c01e28cd4d80d323098fa/example/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazsolar/WeekDatePicker/3595a5570c840282bc0c01e28cd4d80d323098fa/example/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazsolar/WeekDatePicker/3595a5570c840282bc0c01e28cd4d80d323098fa/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /example/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Week Date Picker 3 | 4 | -------------------------------------------------------------------------------- /example/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazsolar/WeekDatePicker/3595a5570c840282bc0c01e28cd4d80d323098fa/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jun 11 14:02:46 CEST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /images/example_screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazsolar/WeekDatePicker/3595a5570c840282bc0c01e28cd4d80d323098fa/images/example_screenshot.gif -------------------------------------------------------------------------------- /images/example_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazsolar/WeekDatePicker/3595a5570c840282bc0c01e28cd4d80d323098fa/images/example_screenshot.png -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | dependencies { 7 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' 8 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.6' 9 | } 10 | } 11 | 12 | apply plugin: 'com.android.library' 13 | apply plugin: 'com.github.dcendents.android-maven' 14 | apply plugin: 'com.jfrog.bintray' 15 | 16 | group = 'solar.blaz' 17 | version = '1.2' 18 | 19 | android { 20 | compileSdkVersion 'Google Inc.:Google APIs:23' 21 | buildToolsVersion "23.0.3" 22 | 23 | defaultConfig { 24 | minSdkVersion 15 25 | targetSdkVersion 23 26 | versionCode 3 27 | versionName project.version 28 | } 29 | 30 | buildTypes { 31 | release { 32 | minifyEnabled false 33 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 34 | } 35 | } 36 | } 37 | 38 | dependencies { 39 | compile 'com.android.support:appcompat-v7:23.4.0' 40 | compile 'org.threeten:threetenbp:1.3.1' 41 | } 42 | 43 | bintray { 44 | user = bintrayUser 45 | key = bintrayKey 46 | 47 | publish = true 48 | 49 | configurations = ['archives'] //When uploading configuration files 50 | pkg { 51 | repo = 'maven' 52 | name = 'week-date-picker' 53 | desc = 'Lightweight week date picker implmentation for Android.' 54 | websiteUrl = "https://github.com/blazsolar/WeekDatePicker" 55 | issueTrackerUrl = 'https://github.com/blazsolar/WeekDatePicker/issues' 56 | vcsUrl = "https://github.com/blazsolar/WeekDatePicker" 57 | licenses = ['MIT'] 58 | labels = ['aar', 'android', 'date'] 59 | publicDownloadNumbers = true 60 | 61 | githubRepo = 'blazsolar/WeekDatePicker' 62 | githubReleaseNotesFile = 'README.md' 63 | } 64 | } 65 | 66 | install { 67 | repositories.mavenInstaller { 68 | pom { 69 | project { 70 | packaging 'aar' 71 | name 'Lightweight week date picker implmentation for Android.' 72 | url "https://github.com/blazsolar/WeekDatePicker" 73 | licenses { 74 | license { 75 | name 'The MIT License (MIT)' 76 | url 'http://rem.mit-license.org' 77 | } 78 | } 79 | developers { 80 | developer { 81 | id "blazsolar" 82 | name "Blaz Solar" 83 | } 84 | } 85 | scm { 86 | url "https://github.com/blazsolar/WeekDatePicker" 87 | connection "scm:git@github.com:blazsolar/WeekDatePicker.git" 88 | developerConnection "scm:git@github.com:blazsolar/WeekDatePicker.git" 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | task sourcesJar(type: Jar) { 96 | from android.sourceSets.main.java.srcDirs 97 | classifier = 'sources' 98 | } 99 | 100 | task javadoc(type: Javadoc) { 101 | source = android.sourceSets.main.java.srcDirs 102 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 103 | } 104 | 105 | task javadocJar(type: Jar, dependsOn: javadoc) { 106 | classifier = 'javadoc' 107 | from javadoc.destinationDir 108 | } 109 | artifacts { 110 | archives javadocJar 111 | archives sourcesJar 112 | } 113 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /library/src/main/java/solar/blaz/date/week/WeekDatePicker.java: -------------------------------------------------------------------------------- 1 | package solar.blaz.date.week; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.res.ColorStateList; 6 | import android.content.res.TypedArray; 7 | import android.graphics.Canvas; 8 | import android.graphics.Color; 9 | import android.graphics.Paint; 10 | import android.graphics.Paint.FontMetricsInt; 11 | import android.graphics.Paint.Style; 12 | import android.graphics.Rect; 13 | import android.graphics.drawable.Drawable; 14 | import android.os.Build; 15 | import android.os.Parcel; 16 | import android.os.Parcelable; 17 | import android.support.annotation.NonNull; 18 | import android.support.annotation.Nullable; 19 | import android.support.v4.text.TextDirectionHeuristicCompat; 20 | import android.support.v4.text.TextDirectionHeuristicsCompat; 21 | import android.text.BoringLayout; 22 | import android.text.Layout; 23 | import android.text.TextPaint; 24 | import android.text.TextUtils; 25 | import android.util.AttributeSet; 26 | import android.util.SparseBooleanArray; 27 | import android.view.KeyEvent; 28 | import android.view.MotionEvent; 29 | import android.view.VelocityTracker; 30 | import android.view.View; 31 | import android.view.ViewConfiguration; 32 | import android.view.animation.DecelerateInterpolator; 33 | import android.widget.OverScroller; 34 | 35 | import org.threeten.bp.DayOfWeek; 36 | import org.threeten.bp.LocalDate; 37 | import org.threeten.bp.format.TextStyle; 38 | import org.threeten.bp.temporal.ChronoUnit; 39 | import org.threeten.bp.temporal.TemporalAdjusters; 40 | 41 | import java.util.ArrayList; 42 | import java.util.List; 43 | import java.util.Locale; 44 | 45 | /** 46 | * Created by Blaž Šolar on 24/01/14. 47 | */ 48 | public class WeekDatePicker extends View { 49 | 50 | public static final String TAG = "DatePicker"; 51 | 52 | /** 53 | * The coefficient by which to adjust (divide) the max fling velocity. 54 | */ 55 | private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 4; 56 | 57 | /** 58 | * The the duration for adjusting the selector wheel. 59 | */ 60 | private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; 61 | 62 | /** 63 | * Determines speed during touch scrolling. 64 | */ 65 | private VelocityTracker velocityTracker; 66 | 67 | /** 68 | * @see ViewConfiguration#getScaledMinimumFlingVelocity() 69 | */ 70 | private int minimumFlingVelocity; 71 | 72 | /** 73 | * @see ViewConfiguration#getScaledMaximumFlingVelocity() 74 | */ 75 | private int maximumFlingVelocity; 76 | 77 | private int touchSlop; 78 | 79 | private final LocalDate today; 80 | private LocalDate firstDay; // first day of week for current date 81 | private final DayOfWeek firstDayOfWeek; 82 | private final BoringLayout[] layouts = new BoringLayout[3 * 7]; // we are drawing 3 weeks at a time on screen 83 | private final BoringLayout[] dayLabelLayouts = new BoringLayout[7]; 84 | 85 | @Nullable private final CharSequence[] labelNames; 86 | 87 | private final TextPaint dayTextPaint; 88 | private final TextPaint dayLabelTextPain; 89 | private final Paint selectedDayColor; 90 | 91 | private BoringLayout.Metrics dayMetrics; 92 | private BoringLayout.Metrics dayLabelMetrics; 93 | 94 | private ColorStateList dayTextColor; 95 | private ColorStateList dayLabelTextColor; 96 | 97 | private TextUtils.TruncateAt ellipsize; 98 | 99 | @Nullable private Drawable dayDrawable; 100 | @Nullable private Drawable indicatorDrawable; 101 | 102 | private int weekWidth; 103 | private int dayWidth; 104 | 105 | private float lastDownEventX; 106 | 107 | private OverScroller flingScrollerX; 108 | private OverScroller adjustScrollerX; 109 | 110 | private int previousScrollerX; 111 | 112 | private boolean scrollingX; 113 | private int scrollPositionStart; 114 | 115 | private OnWeekChanged onWeekChanged; 116 | private OnDateSelected onDateSelected; 117 | 118 | private final SparseBooleanArray dayIndicators = new SparseBooleanArray(); 119 | 120 | private int selectedWeek; 121 | private int selectedDay; 122 | private int pressedDay = Integer.MIN_VALUE; 123 | 124 | private int dayDelta; 125 | 126 | private float dividerSize = 0; 127 | private float labelPadding = 0; 128 | 129 | @Nullable private Rect backgroundRect; 130 | @Nullable private Rect indicatorRect; 131 | 132 | private TextDirectionHeuristicCompat textDir; 133 | 134 | @Nullable private LocalDate fromDate; 135 | @Nullable private LocalDate toDate; 136 | 137 | public WeekDatePicker(Context context) { 138 | this(context, null); 139 | } 140 | 141 | public WeekDatePicker(Context context, AttributeSet attrs) { 142 | this(context, attrs, R.attr.weekDatePickerStyle); 143 | } 144 | 145 | public WeekDatePicker(Context context, AttributeSet attrs, int defStyle) { 146 | super(context, attrs, defStyle); 147 | 148 | // create the selector wheel paint 149 | TextPaint paint = new TextPaint(); 150 | paint.setAntiAlias(true); 151 | dayTextPaint = paint; 152 | 153 | dayLabelTextPain = new TextPaint(); 154 | dayLabelTextPain.setAntiAlias(true); 155 | 156 | selectedDayColor = new Paint(); 157 | selectedDayColor.setColor(Color.RED); 158 | selectedDayColor.setStyle(Style.FILL); 159 | 160 | TypedArray a = context.getTheme().obtainStyledAttributes( 161 | attrs, 162 | R.styleable.WeekDatePicker, 163 | defStyle, 0 164 | ); 165 | 166 | int ellipsize = 3; // END default value 167 | 168 | try { 169 | dayTextColor = a.getColorStateList(R.styleable.WeekDatePicker_android_textColor); 170 | if (dayTextColor == null) { 171 | dayTextColor = ColorStateList.valueOf(Color.BLACK); 172 | } 173 | 174 | dayLabelTextColor = a.getColorStateList(R.styleable.WeekDatePicker_wdp_labelTextColor); 175 | if (dayLabelTextColor == null) { 176 | dayLabelTextColor = ColorStateList.valueOf(Color.BLACK); 177 | } 178 | 179 | ellipsize = a.getInt(R.styleable.WeekDatePicker_android_ellipsize, ellipsize); 180 | dividerSize = a.getDimension(R.styleable.WeekDatePicker_wdp_dividerSize, dividerSize); 181 | 182 | float textSize = a.getDimension(R.styleable.WeekDatePicker_android_textSize, -1); 183 | if(textSize > -1) { 184 | setTextSize(textSize); 185 | } 186 | 187 | float labelTextSize = a.getDimension(R.styleable.WeekDatePicker_wdp_labelTextSize, -1); 188 | if (labelTextSize > -1) { 189 | setLabelTextSize(labelTextSize); 190 | } 191 | 192 | labelPadding = a.getDimension(R.styleable.WeekDatePicker_wdp_labelPadding, labelPadding); 193 | 194 | labelNames = a.getTextArray(R.styleable.WeekDatePicker_wdp_labelNames); 195 | 196 | dayDrawable = a.getDrawable(R.styleable.WeekDatePicker_wdp_dayBackground); 197 | indicatorDrawable = a.getDrawable(R.styleable.WeekDatePicker_wdp_indicatorDrawable); 198 | 199 | int dayOfWeek = a.getInt(R.styleable.WeekDatePicker_wdp_firstDayOfWeek, DayOfWeek.SUNDAY.getValue()); 200 | firstDayOfWeek = DayOfWeek.of(dayOfWeek); 201 | 202 | } finally { 203 | a.recycle(); 204 | } 205 | 206 | switch (ellipsize) { 207 | case 1: 208 | setEllipsize(TextUtils.TruncateAt.START); 209 | break; 210 | case 2: 211 | setEllipsize(TextUtils.TruncateAt.MIDDLE); 212 | break; 213 | case 3: 214 | setEllipsize(TextUtils.TruncateAt.END); 215 | break; 216 | case 4: 217 | setEllipsize(TextUtils.TruncateAt.MARQUEE); 218 | break; 219 | } 220 | 221 | buildFontMetrics(); 222 | buildLabelFontMetrics(); 223 | 224 | // setWillNotDraw(false); 225 | 226 | flingScrollerX = new OverScroller(context); 227 | adjustScrollerX = new OverScroller(context, new DecelerateInterpolator(2.5f)); 228 | 229 | // initialize constants 230 | ViewConfiguration configuration = ViewConfiguration.get(context); 231 | touchSlop = configuration.getScaledTouchSlop(); 232 | minimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); 233 | maximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() 234 | / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT; 235 | 236 | previousScrollerX = Integer.MIN_VALUE; 237 | 238 | calculateItemSize(getWidth(), getHeight()); 239 | 240 | // mTouchHelper = new PickerTouchHelper(this); 241 | // ViewCompat.setAccessibilityDelegate(this, mTouchHelper); 242 | 243 | today = LocalDate.now(); 244 | firstDay = getFirstDay(0); 245 | selectedDay = firstDay.until(today).getDays(); 246 | 247 | } 248 | 249 | @Override 250 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 251 | 252 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 253 | int width = MeasureSpec.getSize(widthMeasureSpec); 254 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 255 | 256 | for (int i = 0; i < 2; i++) { 257 | 258 | if (i != 0 && width == getMeasuredWidth()) { 259 | break; 260 | } 261 | 262 | int height; 263 | if (heightMode == MeasureSpec.EXACTLY) { 264 | height = heightSize; 265 | } else { 266 | int labelTextHeight = Math.abs(dayLabelMetrics.ascent) + Math.abs(dayLabelMetrics.descent); 267 | labelTextHeight += getPaddingTop() + getPaddingBottom(); 268 | 269 | int measuredWidth = getMeasuredWidth(); 270 | if (measuredWidth == 0) { 271 | measuredWidth = width; 272 | } 273 | 274 | int totalHeight = (int) (labelTextHeight + measuredWidth / 7 / 3 * 2 + labelPadding); 275 | 276 | if (heightMode == MeasureSpec.AT_MOST) { 277 | height = Math.min(heightSize, totalHeight); 278 | } else { 279 | height = totalHeight; 280 | } 281 | } 282 | 283 | setMeasuredDimension(width, height); 284 | } 285 | } 286 | 287 | @Override 288 | protected void onDraw(Canvas canvas) { 289 | super.onDraw(canvas); 290 | 291 | float itemWithPadding = weekWidth + dividerSize; 292 | 293 | int saveCount = canvas.getSaveCount(); 294 | canvas.save(); 295 | 296 | int weekOffset = getSelectedWeek() - 1; 297 | float position = itemWithPadding * weekOffset; 298 | 299 | canvas.translate(position, getPaddingTop()); 300 | 301 | for (int i = 0; i < 3; i++) { 302 | drawWeek(canvas, i * 7, weekOffset + i); 303 | canvas.translate(itemWithPadding, 0); 304 | } 305 | 306 | canvas.restoreToCount(saveCount); 307 | 308 | } 309 | 310 | private void drawWeek(Canvas canvas, int layoutIndex, int weekOffset) { 311 | 312 | int saveCount = canvas.save(); 313 | 314 | int labelHeight = dayLabelLayouts[0].getHeight(); 315 | float circleRadius = dayWidth / 3; 316 | int centerY = layouts[0].getHeight() / 2; 317 | float dateLineOffset = circleRadius - centerY; 318 | 319 | for (int i = 0; i < 7; i++) { 320 | 321 | int itemIndex = weekOffset * 7 + i; 322 | BoringLayout layout = layouts[layoutIndex + i]; 323 | BoringLayout labelLayout = dayLabelLayouts[i]; 324 | 325 | dayLabelTextPain.setColor(getTextColor(dayLabelTextColor, itemIndex)); 326 | labelLayout.draw(canvas); 327 | 328 | dayTextPaint.setColor(getTextColor(dayTextColor, itemIndex)); 329 | 330 | int count = canvas.save(); 331 | canvas.translate(0, labelHeight + dateLineOffset + labelPadding); 332 | 333 | if (dayDrawable != null) { 334 | dayDrawable.setBounds(backgroundRect); 335 | dayDrawable.setState(getItemDrawableState(itemIndex)); 336 | dayDrawable.draw(canvas); 337 | } 338 | 339 | if (indicatorDrawable != null && dayIndicators.get(itemIndex - dayDelta, false)) { 340 | indicatorDrawable.setBounds(indicatorRect); 341 | indicatorDrawable.setState(getItemDrawableState(itemIndex)); 342 | indicatorDrawable.draw(canvas); 343 | } 344 | 345 | layout.draw(canvas); 346 | 347 | canvas.restoreToCount(count); 348 | 349 | canvas.translate(dayWidth, 0); 350 | } 351 | 352 | canvas.restoreToCount(saveCount); 353 | 354 | } 355 | 356 | private LocalDate getRelativeFirstDay(int weekOffset) { 357 | weekOffset = getSelectedWeek() + weekOffset; 358 | return getFirstDay(weekOffset); 359 | } 360 | 361 | private LocalDate getFirstDay(int weekOffset) { 362 | return getFirstDay().plusWeeks(weekOffset).with(TemporalAdjusters.previousOrSame(firstDayOfWeek)); 363 | } 364 | 365 | @NonNull private LocalDate getFirstDay() { 366 | if (fromDate != null) { 367 | return fromDate; 368 | } else { 369 | return today; 370 | } 371 | } 372 | 373 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 374 | @Override 375 | public void onRtlPropertiesChanged(int layoutDirection) { 376 | super.onRtlPropertiesChanged(layoutDirection); 377 | 378 | textDir = getTextDirectionHeuristic(); 379 | } 380 | 381 | public void setLimits(@Nullable LocalDate from, @Nullable LocalDate to) { 382 | 383 | readjustIndexes(from); 384 | 385 | fromDate = from; 386 | toDate = to; 387 | 388 | firstDay = getFirstDay(0); 389 | 390 | invalidate(); 391 | } 392 | 393 | private void readjustIndexes(@Nullable LocalDate newDate) { 394 | 395 | LocalDate from = newDate == null ? today : newDate; 396 | LocalDate to = fromDate == null ? today : fromDate; 397 | 398 | int delta = from.until(to).getDays(); 399 | selectedDay += delta; 400 | dayDelta = delta; 401 | 402 | } 403 | 404 | private TextDirectionHeuristicCompat getTextDirectionHeuristic() { 405 | 406 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { 407 | 408 | return TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR; 409 | 410 | } else { 411 | 412 | // Always need to resolve layout direction first 413 | final boolean defaultIsRtl = (getLayoutDirection() == LAYOUT_DIRECTION_RTL); 414 | 415 | switch (getTextDirection()) { 416 | default: 417 | case TEXT_DIRECTION_FIRST_STRONG: 418 | return (defaultIsRtl ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL : 419 | TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR); 420 | case TEXT_DIRECTION_ANY_RTL: 421 | return TextDirectionHeuristicsCompat.ANYRTL_LTR; 422 | case TEXT_DIRECTION_LTR: 423 | return TextDirectionHeuristicsCompat.LTR; 424 | case TEXT_DIRECTION_RTL: 425 | return TextDirectionHeuristicsCompat.RTL; 426 | case TEXT_DIRECTION_LOCALE: 427 | return TextDirectionHeuristicsCompat.LOCALE; 428 | } 429 | } 430 | } 431 | 432 | private void remakeLayout() { 433 | 434 | if (getWidth() > 0) { 435 | 436 | LocalDate day = getRelativeFirstDay(-1); 437 | 438 | for (int i = 0; i < layouts.length; i++) { 439 | 440 | String dayText = String.valueOf(day.getDayOfMonth()); 441 | if (layouts[i] == null) { 442 | layouts[i] = BoringLayout.make(dayText, dayTextPaint, dayWidth, 443 | Layout.Alignment.ALIGN_CENTER, 1f, 1f, dayMetrics, false, ellipsize, 444 | dayWidth); 445 | } else { 446 | layouts[i].replaceOrMake(dayText, dayTextPaint, dayWidth, 447 | Layout.Alignment.ALIGN_CENTER, 1f, 1f, dayMetrics, false, ellipsize, 448 | dayWidth); 449 | } 450 | 451 | day = day.plusDays(1); 452 | } 453 | 454 | DayOfWeek dayOfWeek = firstDayOfWeek; // first index is 1 455 | for (int i = 0; i < dayLabelLayouts.length; i++) { 456 | 457 | CharSequence name; 458 | if (labelNames == null) { 459 | name = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()); 460 | } else { 461 | int index = dayOfWeek.getValue() - 1; 462 | name = labelNames[index]; 463 | } 464 | 465 | 466 | if (dayLabelLayouts[i] == null) { 467 | dayLabelLayouts[i] = BoringLayout.make(name, dayLabelTextPain, dayWidth, 468 | Layout.Alignment.ALIGN_CENTER, 1f, 1f, dayLabelMetrics, false, ellipsize, 469 | dayWidth); 470 | } else { 471 | dayLabelLayouts[i].replaceOrMake(name, dayLabelTextPain, dayWidth, 472 | Layout.Alignment.ALIGN_CENTER, 1f, 1f, dayLabelMetrics, false, ellipsize, 473 | dayWidth); 474 | } 475 | 476 | dayOfWeek = dayOfWeek.plus(1); 477 | 478 | } 479 | 480 | } 481 | 482 | } 483 | 484 | /** 485 | * Calculates text color for specified item based on its position and state. 486 | * 487 | * @param item Index of item to get text color for 488 | * @return Item text color 489 | */ 490 | private int getTextColor(ColorStateList color, int item) { 491 | 492 | List states = new ArrayList<>(); 493 | 494 | if (isItemEnabled(item)) { 495 | states.add(android.R.attr.state_enabled); 496 | } 497 | 498 | if (isItemPressed(item)) { 499 | states.add(android.R.attr.state_pressed); 500 | } 501 | 502 | if (isItemSelected(item)) { 503 | states.add(android.R.attr.state_selected); 504 | } 505 | 506 | int[] finalState = new int[states.size()]; 507 | if (states.size() > 0) { 508 | for (int i = 0; i < states.size(); i++) { 509 | finalState[i] = states.get(i); 510 | } 511 | } 512 | 513 | return color.getColorForState(finalState, color.getDefaultColor()); 514 | 515 | } 516 | 517 | private int[] getItemDrawableState(int item) { 518 | 519 | List state = new ArrayList<>(); 520 | if (isItemEnabled(item)) { 521 | state.add(android.R.attr.state_enabled); 522 | } 523 | 524 | if (isItemSelected(item)) { 525 | state.add(android.R.attr.state_selected); 526 | } 527 | 528 | if (isItemPressed(item)) { 529 | state.add(android.R.attr.state_pressed); 530 | } 531 | 532 | int[] intState = new int[state.size()]; 533 | for (int i = 0; i < state.size(); i++) { 534 | intState[i] = state.get(i); 535 | } 536 | return intState; 537 | 538 | } 539 | 540 | private boolean isItemPressed(int item) { 541 | return item == pressedDay; 542 | } 543 | 544 | private boolean isItemSelected(int item) { 545 | return item == selectedDay; 546 | } 547 | 548 | private boolean isItemEnabled(int item) { 549 | LocalDate date = getDate(item); 550 | 551 | return (fromDate == null || fromDate.isEqual(date) || fromDate.isBefore(date)) 552 | && (toDate == null || date.isEqual(toDate) || toDate.isAfter(date)); 553 | } 554 | 555 | private LocalDate getDate(int day) { 556 | return firstDay.plusDays(day); 557 | } 558 | 559 | @Override 560 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 561 | super.onSizeChanged(w, h, oldw, oldh); 562 | 563 | calculateItemSize(w, h); 564 | calculateBackgroundRect(); 565 | calculateIndicatorRect(); 566 | } 567 | 568 | @Override 569 | public boolean onTouchEvent(MotionEvent event) { 570 | if(!isEnabled()) { 571 | return false; 572 | } 573 | 574 | if (velocityTracker == null) { 575 | velocityTracker = VelocityTracker.obtain(); 576 | } 577 | velocityTracker.addMovement(event); 578 | 579 | int action = event.getActionMasked(); 580 | switch (action) { 581 | case MotionEvent.ACTION_MOVE: 582 | 583 | float currentMoveX = event.getX(); 584 | 585 | int deltaMoveX = (int) (lastDownEventX - currentMoveX); 586 | 587 | if(scrollingX || Math.abs(deltaMoveX) > touchSlop) { 588 | 589 | if(!scrollingX) { 590 | deltaMoveX = 0; 591 | pressedDay = Integer.MIN_VALUE; 592 | scrollingX = true; 593 | getParent().requestDisallowInterceptTouchEvent(true); 594 | scrollPositionStart = getScrollX(); 595 | } 596 | 597 | scrollBy(deltaMoveX, 0); 598 | 599 | lastDownEventX = currentMoveX; 600 | invalidate(); 601 | 602 | } 603 | 604 | break; 605 | case MotionEvent.ACTION_DOWN: 606 | 607 | if(!adjustScrollerX.isFinished()) { 608 | adjustScrollerX.forceFinished(true); 609 | } else if(!flingScrollerX.isFinished()) { 610 | flingScrollerX.forceFinished(true); 611 | } else { 612 | scrollingX = false; 613 | } 614 | 615 | lastDownEventX = event.getX(); 616 | 617 | if(!scrollingX) { 618 | pressedDay = getDayPositionFromTouch(event.getX()); 619 | } 620 | invalidate(); 621 | 622 | break; 623 | case MotionEvent.ACTION_UP: 624 | 625 | VelocityTracker velocityTracker = this.velocityTracker; 626 | velocityTracker.computeCurrentVelocity(500, maximumFlingVelocity); 627 | int initialVelocityX = (int) velocityTracker.getXVelocity(); 628 | 629 | if(scrollingX && Math.abs(initialVelocityX) > minimumFlingVelocity) { 630 | flingX(initialVelocityX); 631 | } else { 632 | float positionX = event.getX(); 633 | if(!scrollingX) { 634 | int itemPos = getDayPositionFromTouch(positionX); 635 | if (isItemEnabled(itemPos)) { 636 | selectDay(itemPos); 637 | } 638 | } else if(scrollingX) { 639 | finishScrolling(); 640 | } 641 | } 642 | 643 | this.velocityTracker.recycle(); 644 | this.velocityTracker = null; 645 | 646 | case MotionEvent.ACTION_CANCEL: 647 | pressedDay = Integer.MIN_VALUE; 648 | invalidate(); 649 | break; 650 | } 651 | 652 | return true; 653 | } 654 | 655 | public void selectDay(@NonNull LocalDate date) { 656 | int day = getDayForDate(date); 657 | selectDay(day); 658 | } 659 | 660 | @Override public void scrollTo(int x, int y) { 661 | if (fromDate != null && x < 0) { 662 | x = 0; 663 | } 664 | 665 | if (toDate != null) { 666 | float totalWidth = (weekWidth + dividerSize) * ChronoUnit.WEEKS.between(getFirstDay(), toDate); 667 | 668 | if (x > totalWidth) { 669 | x = (int) totalWidth; 670 | } 671 | } 672 | 673 | super.scrollTo(x, y); 674 | } 675 | 676 | private void selectDay(final int day) { 677 | 678 | if (selectedDay != day) { 679 | 680 | selectedDay = day; 681 | 682 | // post to the UI Thread to avoid potential interference with the OpenGL Thread 683 | if (onDateSelected != null) { 684 | final LocalDate date = getDate(day); 685 | post(new Runnable() { 686 | @Override 687 | public void run() { 688 | onDateSelected.onDateSelected(date); 689 | } 690 | }); 691 | } 692 | 693 | invalidate(); 694 | 695 | } 696 | 697 | int week = selectedDay / 7; 698 | if (selectedDay < 0 && selectedDay % 7 != 0) { 699 | week -= 1; 700 | } 701 | 702 | if (week != selectedWeek) { 703 | adjustToNearestWeekX(week); 704 | } 705 | 706 | } 707 | 708 | @Override 709 | public boolean onKeyDown(int keyCode, KeyEvent event) { 710 | 711 | if (!isEnabled()) { 712 | return super.onKeyDown(keyCode, event); 713 | } 714 | 715 | switch (keyCode) { 716 | case KeyEvent.KEYCODE_DPAD_CENTER: 717 | case KeyEvent.KEYCODE_ENTER: 718 | // selectDay(); 719 | return true; 720 | case KeyEvent.KEYCODE_DPAD_LEFT: 721 | smoothScrollBy(-1); 722 | return true; 723 | case KeyEvent.KEYCODE_DPAD_RIGHT: 724 | smoothScrollBy(1); 725 | return true; 726 | default: 727 | return super.onKeyDown(keyCode, event); 728 | } 729 | 730 | } 731 | 732 | // @Override 733 | // protected boolean dispatchHoverEvent(MotionEvent event) { 734 | // 735 | // if (mTouchHelper.dispatchHoverEvent(event)) { 736 | // return true; 737 | // } 738 | // 739 | // return super.dispatchHoverEvent(event); 740 | // } 741 | 742 | @Override 743 | public void computeScroll() { 744 | computeScrollX(); 745 | } 746 | 747 | @Override 748 | public void getFocusedRect(Rect r) { 749 | super.getFocusedRect(r); // TODO this should only be current item 750 | } 751 | 752 | public void setOnWeekChangedListener(OnWeekChanged onWeekChanged) { 753 | this.onWeekChanged = onWeekChanged; 754 | } 755 | 756 | public void setOnDateSelectedListener(OnDateSelected onDateSelected) { 757 | this.onDateSelected = onDateSelected; 758 | } 759 | 760 | public int getSelectedWeek() { 761 | int x = getScrollX(); 762 | return getWeekPositionFromCoordinates(x); 763 | } 764 | 765 | public void scrollToWeek(int index) { 766 | selectedWeek = index; 767 | scrollToItem(index); 768 | } 769 | 770 | @Override 771 | protected void onRestoreInstanceState(Parcelable state) { 772 | 773 | if (!(state instanceof SavedState)) { 774 | super.onRestoreInstanceState(state); 775 | return; 776 | } 777 | 778 | SavedState ss = (SavedState) state; 779 | super.onRestoreInstanceState(ss.getSuperState()); 780 | 781 | scrollToWeek(ss.mSelItem); 782 | 783 | 784 | } 785 | 786 | @Override 787 | protected Parcelable onSaveInstanceState() { 788 | Parcelable superState = super.onSaveInstanceState(); 789 | 790 | SavedState savedState = new SavedState(superState); 791 | savedState.mSelItem = selectedWeek; 792 | 793 | return savedState; 794 | 795 | } 796 | 797 | public TextUtils.TruncateAt getEllipsize() { 798 | return ellipsize; 799 | } 800 | 801 | public void setEllipsize(TextUtils.TruncateAt ellipsize) { 802 | if (this.ellipsize != ellipsize) { 803 | this.ellipsize = ellipsize; 804 | 805 | remakeLayout(); 806 | invalidate(); 807 | } 808 | } 809 | 810 | public void setDateIndicator(@NonNull LocalDate date, boolean enabled) { 811 | int itemIndex = getDayForDate(date); 812 | if (enabled) { 813 | dayIndicators.put(itemIndex, true); 814 | } else { 815 | dayIndicators.delete(itemIndex); 816 | } 817 | } 818 | 819 | private int getDayForDate(@NonNull LocalDate date) { 820 | return firstDay.until(date).getDays(); 821 | } 822 | 823 | @Override 824 | protected void drawableStateChanged() { 825 | super.drawableStateChanged(); //TODO 826 | } 827 | 828 | @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { 829 | super.onScrollChanged(l, t, oldl, oldt); 830 | 831 | if (getWeekPositionFromCoordinates(l) != getWeekPositionFromCoordinates(oldl)) { 832 | remakeLayout(); 833 | } 834 | } 835 | 836 | private int getDayPositionFromTouch(float x) { 837 | int weekPositionFromTouch = getWeekPositionFromTouch(x); 838 | int position = weekPositionFromTouch * 7 + getRelativeDayPositionFromTouch(x); 839 | if (weekPositionFromTouch < 0) { 840 | position += 6; 841 | } 842 | return position; 843 | } 844 | 845 | private int getRelativeDayPositionFromTouch(float x) { 846 | return getRelativeDayPositionFromCoordinates((int) (getScrollX() + x)); 847 | } 848 | 849 | private int getWeekPositionFromTouch(float x) { 850 | return getWeekPositionFromCoordinates((int) (getScrollX() - (weekWidth + dividerSize) * .5f + x)); 851 | } 852 | 853 | /** 854 | * Returns day of the week for passed coordinates. 855 | */ 856 | private int getRelativeDayPositionFromCoordinates(int x) { 857 | float relativePosition = x % (weekWidth + dividerSize); 858 | return (int) (relativePosition / dayWidth); 859 | } 860 | 861 | private void computeScrollX() { 862 | OverScroller scroller = flingScrollerX; 863 | if(scroller.isFinished()) { 864 | scroller = adjustScrollerX; 865 | if(scroller.isFinished()) { 866 | return; 867 | } 868 | } 869 | 870 | if(scroller.computeScrollOffset()) { 871 | 872 | int currentScrollerX = scroller.getCurrX(); 873 | if(previousScrollerX == Integer.MIN_VALUE) { 874 | previousScrollerX = scroller.getStartX(); 875 | } 876 | 877 | scrollBy(currentScrollerX - previousScrollerX, 0); 878 | previousScrollerX = currentScrollerX; 879 | 880 | if(scroller.isFinished()) { 881 | onScrollerFinishedX(scroller); 882 | } 883 | 884 | postInvalidate(); 885 | } 886 | } 887 | 888 | private void flingX(int velocityX) { 889 | 890 | int signum= Integer.signum(velocityX); 891 | 892 | int currentWeekPosition = getWeekPositionFromCoordinates(scrollPositionStart); 893 | float weekWidth = this.weekWidth + dividerSize; 894 | 895 | int finalPosition; 896 | switch (signum) { 897 | case -1: 898 | finalPosition = (int) weekWidth * (currentWeekPosition + 1); 899 | break; 900 | default: 901 | case 0: 902 | finalPosition = (int) weekWidth * (currentWeekPosition); 903 | break; 904 | case 1: 905 | finalPosition = (int) weekWidth * (currentWeekPosition - 1); 906 | break; 907 | } 908 | 909 | int dx = finalPosition - getScrollX(); 910 | 911 | previousScrollerX = Integer.MIN_VALUE; 912 | flingScrollerX.startScroll(getScrollX(), getScrollY(), dx, 0); 913 | 914 | invalidate(); 915 | } 916 | 917 | private void adjustToNearestWeekX() { 918 | 919 | int x = getScrollX(); 920 | int week = Math.round(x / (weekWidth + dividerSize * 1f)); 921 | adjustToNearestWeekX(week); 922 | 923 | } 924 | 925 | private void adjustToNearestWeekX(int week) { 926 | 927 | int x = getScrollX(); 928 | 929 | if (selectedWeek != week) { 930 | selectedWeek = week; 931 | notifyWeekChange(); 932 | } 933 | 934 | int weekPosition = (weekWidth + (int) dividerSize) * week; 935 | 936 | int deltaX = weekPosition - x; 937 | 938 | previousScrollerX = Integer.MIN_VALUE; 939 | adjustScrollerX.startScroll(x, 0, deltaX, 0, SELECTOR_ADJUSTMENT_DURATION_MILLIS); 940 | invalidate(); 941 | } 942 | 943 | private void calculateItemSize(int w, int h) { 944 | 945 | int items = 1; 946 | int totalPadding = ((int) dividerSize * (items - 1)); 947 | weekWidth = (w - totalPadding) / items; 948 | dayWidth = weekWidth / 7; 949 | 950 | scrollToItem(selectedWeek); 951 | 952 | buildFontMetrics(); 953 | buildLabelFontMetrics(); 954 | remakeLayout(); 955 | 956 | } 957 | 958 | private void calculateBackgroundRect() { 959 | 960 | if (dayDrawable != null) { 961 | float circleRadius = dayWidth / 3; 962 | int centerX = layouts[0].getWidth() / 2; 963 | int centerY = layouts[0].getHeight() / 2; 964 | 965 | backgroundRect = new Rect((int) (centerX - circleRadius), (int) (centerY - circleRadius), 966 | (int) (centerX + circleRadius), (int) (centerY + circleRadius)); 967 | } else { 968 | backgroundRect = null; 969 | } 970 | 971 | } 972 | 973 | private void calculateIndicatorRect() { 974 | 975 | if (indicatorDrawable != null) { 976 | 977 | float circleRadius = dayWidth / 3; 978 | int centerX = layouts[0].getWidth() / 2; 979 | int centerY = layouts[0].getHeight() / 2; 980 | 981 | int indicatorDotWidth = indicatorDrawable.getIntrinsicWidth(); 982 | int indicatorDotHeight = indicatorDrawable.getIntrinsicHeight(); 983 | indicatorRect = new Rect(centerX - indicatorDotWidth / 2, 984 | (int) (centerY + circleRadius - indicatorDotHeight), 985 | centerX + indicatorDotWidth / 2, (int) (centerY + circleRadius)); 986 | 987 | } else { 988 | indicatorRect = null; 989 | } 990 | 991 | } 992 | 993 | private void onScrollerFinishedX(OverScroller scroller) { 994 | if(scroller == flingScrollerX) { 995 | finishScrolling(); 996 | } 997 | } 998 | 999 | private void finishScrolling() { 1000 | 1001 | adjustToNearestWeekX(); 1002 | scrollingX = false; 1003 | } 1004 | 1005 | private int getPositionOnScreen(float x) { 1006 | return (int) (x / (weekWidth + dividerSize)); 1007 | } 1008 | 1009 | private void smoothScrollBy(int i) { 1010 | int deltaMoveX = (weekWidth + (int) dividerSize) * i; 1011 | 1012 | previousScrollerX = Integer.MIN_VALUE; 1013 | flingScrollerX.startScroll(getScrollX(), 0, deltaMoveX, 0); 1014 | invalidate(); 1015 | } 1016 | 1017 | /** 1018 | * Sets text size for items 1019 | * @param size New item text size in px. 1020 | */ 1021 | private void setTextSize(float size) { 1022 | if(size != dayTextPaint.getTextSize()) { 1023 | dayTextPaint.setTextSize(size); 1024 | 1025 | buildFontMetrics(); 1026 | requestLayout(); 1027 | invalidate(); 1028 | } 1029 | } 1030 | 1031 | private void setLabelTextSize(float size) { 1032 | if (size != dayLabelTextPain.getTextSize()) { 1033 | dayLabelTextPain.setTextSize(size); 1034 | 1035 | buildLabelFontMetrics(); 1036 | remakeLayout(); 1037 | invalidate(); 1038 | } 1039 | } 1040 | 1041 | /** 1042 | * Calculates item from x coordinate position. 1043 | * @param x Scroll position to calculate. 1044 | * @return Selected item from scrolling position in {param x} 1045 | */ 1046 | private int getWeekPositionFromCoordinates(int x) { 1047 | return Math.round(x / (weekWidth + dividerSize)); 1048 | } 1049 | 1050 | /** 1051 | * Scrolls to specified item. 1052 | * @param index Index of an item to scroll to 1053 | */ 1054 | private void scrollToItem(int index) { 1055 | scrollTo((weekWidth + (int) dividerSize) * index, 0); 1056 | } 1057 | 1058 | private void notifyWeekChange() { 1059 | 1060 | // post to the UI Thread to avoid potential interference with the OpenGL Thread 1061 | if (onWeekChanged != null) { 1062 | post(new Runnable() { 1063 | @Override 1064 | public void run() { 1065 | LocalDate firstDay = getFirstDay(getWeekPositionFromCoordinates(getScrollX())); 1066 | onWeekChanged.onItemSelected(firstDay); 1067 | } 1068 | }); 1069 | } 1070 | 1071 | } 1072 | 1073 | private void buildFontMetrics() { 1074 | FontMetricsInt fontMetricsInt = dayTextPaint.getFontMetricsInt(); 1075 | dayMetrics = WeekDatePicker.toBoringFontMetrics(fontMetricsInt, dayMetrics); 1076 | dayMetrics.width = weekWidth; 1077 | } 1078 | 1079 | private void buildLabelFontMetrics() { 1080 | FontMetricsInt fontMetricsInt = dayLabelTextPain.getFontMetricsInt(); 1081 | dayLabelMetrics = WeekDatePicker.toBoringFontMetrics(fontMetricsInt, dayLabelMetrics); 1082 | dayLabelMetrics.width = weekWidth; 1083 | } 1084 | 1085 | public interface OnWeekChanged { 1086 | 1087 | void onItemSelected(LocalDate firstDay); 1088 | 1089 | } 1090 | 1091 | public interface OnDateSelected { 1092 | 1093 | void onDateSelected(LocalDate date); 1094 | 1095 | } 1096 | 1097 | private static BoringLayout.Metrics toBoringFontMetrics(FontMetricsInt metrics, 1098 | @Nullable BoringLayout.Metrics fontMetrics) { 1099 | 1100 | if (fontMetrics == null) { 1101 | fontMetrics = new BoringLayout.Metrics(); 1102 | } 1103 | 1104 | fontMetrics.ascent = metrics.ascent; 1105 | fontMetrics.bottom = metrics.bottom; 1106 | fontMetrics.descent = metrics.descent; 1107 | fontMetrics.leading = metrics.leading; 1108 | fontMetrics.top = metrics.top; 1109 | return fontMetrics; 1110 | } 1111 | 1112 | public static class SavedState extends BaseSavedState { 1113 | 1114 | private int mSelItem; 1115 | 1116 | public SavedState(Parcelable superState) { 1117 | super(superState); 1118 | } 1119 | 1120 | private SavedState(Parcel in) { 1121 | super(in); 1122 | mSelItem = in.readInt(); 1123 | } 1124 | 1125 | @Override 1126 | public void writeToParcel(Parcel dest, int flags) { 1127 | super.writeToParcel(dest, flags); 1128 | 1129 | dest.writeInt(mSelItem); 1130 | } 1131 | 1132 | @Override 1133 | public String toString() { 1134 | return "HorizontalPicker.SavedState{" 1135 | + Integer.toHexString(System.identityHashCode(this)) 1136 | + " selItem=" + mSelItem 1137 | + "}"; 1138 | } 1139 | 1140 | @SuppressWarnings("hiding") 1141 | public static final Creator CREATOR 1142 | = new Creator() { 1143 | public SavedState createFromParcel(Parcel in) { 1144 | return new SavedState(in); 1145 | } 1146 | 1147 | public SavedState[] newArray(int size) { 1148 | return new SavedState[size]; 1149 | } 1150 | }; 1151 | } 1152 | 1153 | } 1154 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library' 2 | include ':example' 3 | 4 | project(':library').name = 'week-date-picker' 5 | --------------------------------------------------------------------------------