├── lib ├── .gitignore ├── gradle.properties ├── src │ ├── main │ │ ├── res │ │ │ └── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── styles.xml │ │ │ │ └── attrs.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── andrewgiang │ │ │ └── textspritzer │ │ │ └── lib │ │ │ ├── DefaultDelayStrategy.java │ │ │ ├── DelayStrategy.java │ │ │ ├── SpritzerTextView.java │ │ │ └── Spritzer.java │ └── instrumentTest │ │ └── java │ │ └── com │ │ └── andrewgiang │ │ └── textspritzer │ │ └── lib │ │ └── SpritzerTest.java ├── build.gradle └── proguard-rules.txt ├── sample ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── drawable-hdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-mdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-xhdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── values │ │ │ ├── styles.xml │ │ │ ├── dimens.xml │ │ │ └── strings.xml │ │ ├── values-w820dp │ │ │ └── dimens.xml │ │ └── layout │ │ │ └── activity_main.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── andrewgiang │ │ └── textspritzer │ │ └── app │ │ └── MainActivity.java ├── build.gradle └── proguard-rules.txt ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .travis.yml ├── .gitignore ├── README.md ├── gradle.properties ├── QUICKSTART.md ├── gradlew.bat └── gradlew /lib/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':lib', ':sample' 2 | -------------------------------------------------------------------------------- /lib/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=SpritzerTextView Library 2 | POM_ARTIFACT_ID=library 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /lib/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TextSpritzer 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgiang/SpritzerTextView/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /lib/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #fff7000e 4 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgiang/SpritzerTextView/HEAD/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgiang/SpritzerTextView/HEAD/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgiang/SpritzerTextView/HEAD/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgiang/SpritzerTextView/HEAD/sample/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /lib/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SpritzerTextView Demo 5 | Text Size 6 | WPM 7 | 8 | 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-all.zip 7 | -------------------------------------------------------------------------------- /lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /lib/src/main/java/com/andrewgiang/textspritzer/lib/DefaultDelayStrategy.java: -------------------------------------------------------------------------------- 1 | package com.andrewgiang.textspritzer.lib; 2 | 3 | /** 4 | * Created by andrewgiang on 3/19/14. 5 | */ 6 | public class DefaultDelayStrategy implements DelayStrategy { 7 | @Override 8 | public int delayMultiplier(String word) { 9 | if (word.length() >= 6 || word.contains(",") || word.contains(":") || word.contains(";") || word.contains(".") || word.contains("?") || word.contains("!") || word.contains("\"")) { 10 | return 3; 11 | } 12 | return 1; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'android-library' 2 | 3 | android { 4 | compileSdkVersion 19 5 | buildToolsVersion "19.0.1" 6 | 7 | defaultConfig { 8 | minSdkVersion 10 9 | targetSdkVersion 19 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | release { 14 | runProguard false 15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 16 | } 17 | } 18 | 19 | dependencies { 20 | compile 'com.android.support:appcompat-v7:+' 21 | compile fileTree(dir: 'libs', include: ['*.jar']) 22 | } 23 | apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle' -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'android' 2 | 3 | android { 4 | compileSdkVersion 19 5 | buildToolsVersion "19.0.1" 6 | 7 | defaultConfig { 8 | minSdkVersion 10 9 | targetSdkVersion 19 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | runProguard false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | //compile 'com.andrewgiang.spritzertextview:library:0.0.1' 23 | compile project(":lib") 24 | compile 'com.android.support:appcompat-v7:+' 25 | compile fileTree(dir: 'libs', include: ['*.jar']) 26 | } 27 | -------------------------------------------------------------------------------- /lib/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the ProGuard 5 | # include property in project.properties. 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 | #} -------------------------------------------------------------------------------- /sample/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the ProGuard 5 | # include property in project.properties. 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 | #} -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: oraclejdk7 3 | before_install: 4 | # Install base Android SDK and components 5 | - sudo apt-get install -qq libstdc++6:i386 lib32z1 6 | - export COMPONENTS=build-tools-19.0.1,android-19,sysimg-19,extra-android-m2repository,extra-android-support 7 | - curl -L https://raw.github.com/embarkmobile/android-sdk-installer/version-1/android-sdk-installer | bash /dev/stdin --install=$COMPONENTS 8 | - source ~/.android-sdk-installer/env 9 | - export TERM=dumb # to get clean gradle output 10 | # Create and start emulator 11 | - android list targets # for debugging 12 | - echo no | android create avd --force -n test -t android-19 --abi armeabi-v7a 13 | - emulator -avd test -no-skin -no-audio -no-window & 14 | 15 | before_script: 16 | - wait_for_emulator 17 | script: 18 | - ./gradlew connectedInstrumentTest -------------------------------------------------------------------------------- /lib/src/main/java/com/andrewgiang/textspritzer/lib/DelayStrategy.java: -------------------------------------------------------------------------------- 1 | package com.andrewgiang.textspritzer.lib; 2 | 3 | /** 4 | * Created by andrewgiang on 3/19/14. 5 | */ 6 | public interface DelayStrategy { 7 | 8 | /** 9 | * A delay strategy for @see{@link Spritzer#processNextWord()} that 10 | * will determine how long the Thread 11 | * sleeps after a word is being processed. This delay time 12 | * is calculated by multiplying the @see{@link Spritzer#getInterWordDelay()} 13 | * with the return value from this method. 14 | *

15 | * The default strategy can be found @ 16 | * 17 | * @param word the word to be checked for a possible delay multiplier 18 | * @return int multiplier 19 | * @see {@link com.andrewgiang.textspritzer.lib.DefaultDelayStrategy} 20 | */ 21 | public int delayMultiplier(String word); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by http://www.gitignore.io 2 | 3 | ### Android ### 4 | # Built application files 5 | *.apk 6 | *.ap_ 7 | 8 | # Files for the Dalvik VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | 29 | ### Intellij ### 30 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode 31 | 32 | ## Directory-based project format 33 | .idea/ 34 | # if you remove the above rule, at least ignore user-specific stuff: 35 | # .idea/workspace.xml 36 | # .idea/tasks.xml 37 | # and these sensitive or high-churn files: 38 | # .idea/dataSources.ids 39 | # .idea/dataSources.xml 40 | # .idea/sqlDataSources.xml 41 | # .idea/dynamic.xml 42 | 43 | ## File-based project format 44 | *.ipr 45 | *.iws 46 | *.iml 47 | 48 | ## Additional for IntelliJ 49 | out/ 50 | 51 | # generated by mpeltonen/sbt-idea plugin 52 | .idea_modules/ 53 | 54 | # generated by JIRA plugin 55 | atlassian-ide-plugin.xml 56 | 57 | # generated by Crashlytics plugin (for Android Studio and Intellij) 58 | com_crashlytics_export_strings.xml 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SpritzerTextView 2 | ============ 3 | 4 | [![Build Status](https://travis-ci.org/andrewgiang/SpritzerTextView.png?branch=master)](https://travis-ci.org/andrewgiang/SpritzerTextView) 5 | A simplified TextView wrapper that originated from the Spritzer in [OpenSpritz-Android](https://github.com/OnlyInAmerica/OpenSpritz-Android) to "spritz" text. 6 | 7 | Note: This library has nothing to do with SpritzInc. 8 | ###### This library is a preview, api may change before stable release. 9 | 10 | ![SpritzerTextView example](http://i.imgur.com/mkeViYY.gif) 11 | 12 | 13 | 14 | Quick Start 15 | ------------ 16 | To use in your project please take a look at the [Quick Start Guide](https://github.com/andrewgiang/SpritzerTextView/blob/master/QUICKSTART.md). 17 | 18 | Contributing 19 | ------------ 20 | Please feel free to fork and contribute in any way. 21 | 22 | 23 | License 24 | ------------ 25 | ``` 26 | Copyright [2014] [Andrew Giang] 27 | 28 | Licensed under the Apache License, Version 2.0 (the "License"); 29 | you may not use this file except in compliance with the License. 30 | You may obtain a copy of the License at 31 | 32 | http://www.apache.org/licenses/LICENSE-2.0 33 | 34 | Unless required by applicable law or agreed to in writing, software 35 | distributed under the License is distributed on an "AS IS" BASIS, 36 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | See the License for the specific language governing permissions and 38 | limitations under the License. 39 | ``` 40 | 41 | 42 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/andrewgiang/spritzertextview/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 43 | 44 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Settings specified in this file will override any Gradle settings 5 | # configured through the IDE. 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 | 20 | VERSION_NAME=0.3.4 21 | VERSION_CODE=8 22 | GROUP=com.andrewgiang.spritzertextview 23 | 24 | POM_DESCRIPTION=A simple TextView wrapper for the OpenSpritz-Android project 25 | POM_URL=https://github.com/andrewgiang/SpritzerTextView 26 | POM_SCM_URL=https://github.com/andrewgiang/SpritzerTextView 27 | POM_SCM_CONNECTION=scm:git@github.com:andrewgiang/SpritzerTextView.git 28 | POM_SCM_DEV_CONNECTION=scm:git@github.com:andrewgiang/SpritzerTextView.git 29 | POM_LICENCE_NAME=Apache License, Version 2 30 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 31 | POM_LICENCE_DIST=repo 32 | POM_DEVELOPER_ID=andrewgiang 33 | POM_DEVELOPER_NAME=Andrew Giang 34 | 35 | ANDROID_BUILD_TARGET_SDK_VERSION=19 36 | ANDROID_BUILD_TOOLS_VERSION=19 37 | ANDROID_BUILD_SDK_VERSION=19 -------------------------------------------------------------------------------- /QUICKSTART.md: -------------------------------------------------------------------------------- 1 | Quick Start Guide 2 | =========== 3 | 4 | Usage 5 | ------------ 6 | 7 | This library is released to maven central as an `aar` so all you need to do is add the following dependency to your `build.gradle` 8 | 9 | ``` 10 | dependencies { 11 | compile 'com.andrewgiang.spritzertextview:library:(insert latest version)' 12 | } 13 | ``` 14 | 15 | ### Add the SpritzerTextView to your layout 16 | 17 | ``` 18 | 23 | ``` 24 | ###### Note: 25 | * Font must be a monospaced type font 26 | * Add ```xmlns:app="http://schemas.android.com/apk/res-auto"``` to your root layout to use `clickControls` 27 | 28 | 29 | 30 | #### Retrieve the view 31 | 32 | ``` 33 | final SpritzerTextView spritzerTV = (SpritzerTextView) findViewById(R.id.spritzTV); 34 | 35 | ``` 36 | #### Set Spritzter Text 37 | ``` 38 | spritzerTV.setSpritzText("add the spritz text here"); 39 | ``` 40 | 41 | #### Play and Pause the Spritzer 42 | 43 | ``` 44 | spritzerTV.play(); // Play the text set in method setSpritzText() 45 | spritzerTV.pause(); // Pauses the spritzer can be resumed with the play() method 46 | ``` 47 | 48 | Customizations 49 | ------------ 50 | 51 | ### Set a `OnClickControlsListener` listener 52 | ``` 53 | spritzerTV.setOnClickControlListener(new SpritzerTextView.OnClickControlListener() { 54 | 55 | /** This listener will be called when a user clicks on the 56 | * TextView to play or pause the spritzer, it will only work 57 | * if clickControls are enabled 58 | */ 59 | @Override 60 | public void onPause() { 61 | Toast.makeText(MainActivity.this, "Spritzer has been paused", Toast.LENGTH_SHORT).show(); 62 | 63 | } 64 | 65 | @Override 66 | public void onPlay() { 67 | Toast.makeText(MainActivity.this, "Spritzer is playing", Toast.LENGTH_SHORT).show(); 68 | 69 | } 70 | }); 71 | ``` 72 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz 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 | -------------------------------------------------------------------------------- /lib/src/instrumentTest/java/com/andrewgiang/textspritzer/lib/SpritzerTest.java: -------------------------------------------------------------------------------- 1 | package com.andrewgiang.textspritzer.lib; 2 | 3 | import android.test.AndroidTestCase; 4 | import android.widget.TextView; 5 | 6 | /** 7 | * Created by andrewgiang on 3/11/14. 8 | */ 9 | public class SpritzerTest extends AndroidTestCase { 10 | 11 | 12 | public static final String SEPARATOR = ":"; 13 | 14 | /** 15 | * Note: MAX_WORD_LENGTH = 13 16 | *

17 | * Test cases are in this format: 18 | * "WordToTest"+ SEPARATOR + 19 | * "FirstHalfWord"+ SEPARATOR + 20 | * "SecondHalfWord", 21 | */ 22 | public static String[] splitWordTests = { 23 | 24 | 25 | /** 26 | * Test a long word > MAX_WORD_LENGTH && word < MAX_WORD_LENGTH * 2 27 | * 28 | * Split index should be the word.length/2 29 | */ 30 | "abcdefghijklmnopqrstuv" + SEPARATOR + 31 | "abcdefghijk-" + SEPARATOR + 32 | "lmnopqrstuv", 33 | 34 | /** 35 | * Test a word > MAX_WORD_LENGTH with a hyphen 36 | * 37 | * Split index should be after the hypen 38 | */ 39 | "hyperactive-monkey" + SEPARATOR + 40 | "hyperactive-" + SEPARATOR + 41 | "monkey", 42 | 43 | /** 44 | * Test a word with length > MAX_WORD_LENGTH and has a period 45 | * 46 | * Split index should be after the period 47 | */ 48 | "abcdefghijk.lmnopqrstuv" + SEPARATOR + 49 | "abcdefghijk." + SEPARATOR + 50 | "lmnopqrstuv", 51 | 52 | /** 53 | * Test a word longer than 26 (MAX_WORD_LENGTH *2) 54 | * 55 | * Split index should be MAX_WORD_LENGTH - 1 so we can add a hypen to make it 13 56 | */ 57 | "abcdefghijklmnopqrstuvwxyz0" + SEPARATOR + 58 | "abcdefghijkl-" + SEPARATOR + 59 | "mnopqrstuvwxyz0" 60 | }; 61 | private Spritzer spritzer; 62 | 63 | @Override 64 | protected void setUp() throws Exception { 65 | super.setUp(); 66 | spritzer = new Spritzer(new TextView(getContext())); 67 | } 68 | 69 | 70 | public void testSplitWordList() { 71 | for (String tests : splitWordTests) { 72 | final String[] split = tests.split(":"); 73 | assertEquals(3, split.length); 74 | } 75 | } 76 | 77 | public void testSplitWords() { 78 | for (String tests : splitWordTests) { 79 | final String[] arry = tests.split(":"); 80 | String longword = arry[0]; 81 | String expectedSplit = arry[1]; 82 | String expectedAddedToQueue = arry[2]; 83 | assertEquals(expectedSplit, spritzer.splitLongWord(longword)); 84 | assertEquals(expectedAddedToQueue, spritzer.mWordQueue.peek()); 85 | spritzer.mWordQueue.clear(); 86 | } 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | 19 | 25 | 26 | 34 | 35 | 45 | 46 | 52 | 53 | 64 | 65 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /sample/src/main/java/com/andrewgiang/textspritzer/app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.andrewgiang.textspritzer.app; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.ActionBarActivity; 5 | import android.widget.ProgressBar; 6 | import android.widget.SeekBar; 7 | import android.widget.Toast; 8 | 9 | import com.andrewgiang.textspritzer.lib.Spritzer; 10 | import com.andrewgiang.textspritzer.lib.SpritzerTextView; 11 | 12 | public class MainActivity extends ActionBarActivity { 13 | 14 | public static final String TAG = MainActivity.class.getName(); 15 | private SpritzerTextView mSpritzerTextView; 16 | private SeekBar mSeekBarTextSize; 17 | private SeekBar mSeekBarWpm; 18 | private ProgressBar mProgressBar; 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | setContentView(R.layout.activity_main); 24 | 25 | //Review the view and set text to be spritzed 26 | mSpritzerTextView = (SpritzerTextView) findViewById(R.id.spritzTV); 27 | mSpritzerTextView.setSpritzText("OpenSpritz has nothing to do with Spritz Incorporated. " + 28 | "This is an open source, community created project, made with love because Spritz is " + 29 | "such an awesome technique for reading with."); 30 | 31 | 32 | //This attaches a progress bar that show exactly how far you are into your spritz 33 | mProgressBar = (ProgressBar) findViewById(R.id.spritz_progress); 34 | mSpritzerTextView.attachProgressBar(mProgressBar); 35 | 36 | 37 | //Set how fast the spritzer should go 38 | mSpritzerTextView.setWpm(500); 39 | 40 | //Set Click Control listeners, these will be called when the user uses the click controls 41 | mSpritzerTextView.setOnClickControlListener(new SpritzerTextView.OnClickControlListener() { 42 | @Override 43 | public void onPause() { 44 | Toast.makeText(MainActivity.this, "Spritzer has been paused", Toast.LENGTH_SHORT).show(); 45 | 46 | } 47 | 48 | @Override 49 | public void onPlay() { 50 | Toast.makeText(MainActivity.this, "Spritzer is playing", Toast.LENGTH_SHORT).show(); 51 | 52 | } 53 | }); 54 | 55 | mSpritzerTextView.setOnCompletionListener(new Spritzer.OnCompletionListener() { 56 | @Override 57 | public void onComplete() { 58 | Toast.makeText(MainActivity.this, "Spritzer is finished", Toast.LENGTH_SHORT).show(); 59 | 60 | } 61 | }); 62 | 63 | // mSpritzerTextView.setDelayStrategy(new DelayStrategy() { 64 | // @Override 65 | // public int delayMultiplier(String word) { 66 | // if(word.contains("-")){ 67 | // return 5; 68 | // } 69 | // return 1; 70 | // } 71 | // }); 72 | 73 | 74 | setupSeekBars(); 75 | 76 | 77 | } 78 | 79 | /** 80 | * This is just shows two seek bars to change wpm and text size 81 | */ 82 | private void setupSeekBars() { 83 | mSeekBarTextSize = (SeekBar) findViewById(R.id.seekBarTextSize); 84 | mSeekBarWpm = (SeekBar) findViewById(R.id.seekBarWpm); 85 | if (mSeekBarWpm != null && mSeekBarTextSize != null) { 86 | mSeekBarWpm.setMax(mSpritzerTextView.getWpm() * 2); 87 | 88 | mSeekBarTextSize.setMax((int) mSpritzerTextView.getTextSize() * 2); 89 | mSeekBarWpm.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 90 | @Override 91 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 92 | if (progress > 0) { 93 | mSpritzerTextView.setWpm(progress); 94 | } 95 | } 96 | 97 | @Override 98 | public void onStartTrackingTouch(SeekBar seekBar) { 99 | } 100 | 101 | @Override 102 | public void onStopTrackingTouch(SeekBar seekBar) { 103 | } 104 | }); 105 | mSeekBarTextSize.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 106 | @Override 107 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 108 | mSpritzerTextView.setTextSize(progress); 109 | 110 | } 111 | 112 | @Override 113 | public void onStartTrackingTouch(SeekBar seekBar) { 114 | } 115 | 116 | @Override 117 | public void onStopTrackingTouch(SeekBar seekBar) { 118 | 119 | } 120 | }); 121 | 122 | mSeekBarWpm.setProgress(mSpritzerTextView.getWpm()); 123 | mSeekBarTextSize.setProgress((int) mSpritzerTextView.getTextSize()); 124 | } 125 | 126 | } 127 | 128 | 129 | } 130 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 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 | -------------------------------------------------------------------------------- /lib/src/main/java/com/andrewgiang/textspritzer/lib/SpritzerTextView.java: -------------------------------------------------------------------------------- 1 | package com.andrewgiang.textspritzer.lib; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Paint; 7 | import android.util.AttributeSet; 8 | import android.util.Log; 9 | import android.util.TypedValue; 10 | import android.view.View; 11 | import android.widget.ProgressBar; 12 | import android.widget.TextView; 13 | 14 | /** 15 | * Created by andrewgiang on 3/3/14. 16 | */ 17 | public class SpritzerTextView extends TextView implements View.OnClickListener { 18 | /** 19 | * Interface definition for a callback to be invoked when the 20 | * clickControls are enabled and the view is clicked 21 | */ 22 | public static interface OnClickControlListener { 23 | /** 24 | * Called when the spritzer pauses upon click 25 | */ 26 | void onPause(); 27 | 28 | /** 29 | * Called when the spritzer plays upon clicked 30 | */ 31 | void onPlay(); 32 | } 33 | 34 | public static final String TAG = SpritzerTextView.class.getName(); 35 | public static final int PAINT_WIDTH_DP = 4; // thickness of spritz guide bars in dp 36 | // For optimal drawing should be an even number 37 | 38 | private Spritzer mSpritzer; 39 | private Paint mPaintGuides; 40 | private float mPaintWidthPx; 41 | private String mTestString; 42 | private boolean mDefaultClickListener = false; 43 | private int mAdditonalPadding; 44 | 45 | /** 46 | * Register a callback for when the view has been clicked 47 | *

48 | * Note: it is mandatory to use the clickControls 49 | * 50 | * @param listener 51 | */ 52 | public void setOnClickControlListener(OnClickControlListener listener) { 53 | mClickControlListener = listener; 54 | } 55 | 56 | private OnClickControlListener mClickControlListener; 57 | 58 | 59 | public SpritzerTextView(Context context) { 60 | super(context); 61 | init(); 62 | } 63 | 64 | public SpritzerTextView(Context context, AttributeSet attrs) { 65 | super(context, attrs); 66 | init(attrs); 67 | } 68 | 69 | public SpritzerTextView(Context context, AttributeSet attrs, int defStyle) { 70 | super(context, attrs, defStyle); 71 | init(attrs); 72 | } 73 | 74 | private void init(AttributeSet attrs) { 75 | setAdditionalPadding(attrs); 76 | final TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.SpritzerTextView, 0, 0); 77 | try { 78 | mDefaultClickListener = a.getBoolean(R.styleable.SpritzerTextView_clickControls, false); 79 | } finally { 80 | a.recycle(); 81 | } 82 | init(); 83 | 84 | } 85 | 86 | private void setAdditionalPadding(AttributeSet attrs) { 87 | //check padding attributes 88 | int[] attributes = new int[]{android.R.attr.padding, android.R.attr.paddingTop, 89 | android.R.attr.paddingBottom}; 90 | 91 | final TypedArray paddingArray = getContext().obtainStyledAttributes(attrs, attributes); 92 | try { 93 | final int padding = paddingArray.getDimensionPixelOffset(0, 0); 94 | final int paddingTop = paddingArray.getDimensionPixelOffset(1, 0); 95 | final int paddingBottom = paddingArray.getDimensionPixelOffset(2, 0); 96 | mAdditonalPadding = Math.max(padding, Math.max(paddingTop, paddingBottom)); 97 | Log.w(TAG, "Additional Padding " + mAdditonalPadding); 98 | } finally { 99 | paddingArray.recycle(); 100 | } 101 | } 102 | 103 | private void init() { 104 | int pivotPadding = getPivotPadding(); 105 | setPadding(getPaddingLeft(), pivotPadding, getPaddingRight(), pivotPadding); 106 | mPaintWidthPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, PAINT_WIDTH_DP, getResources().getDisplayMetrics()); 107 | mSpritzer = new Spritzer(this); 108 | mPaintGuides = new Paint(Paint.ANTI_ALIAS_FLAG); 109 | mPaintGuides.setColor(getCurrentTextColor()); 110 | mPaintGuides.setStrokeWidth(mPaintWidthPx); 111 | mPaintGuides.setAlpha(128); 112 | if (mDefaultClickListener) { 113 | this.setOnClickListener(this); 114 | } 115 | 116 | } 117 | 118 | @Override 119 | protected void onDraw(Canvas canvas) { 120 | super.onDraw(canvas); 121 | 122 | // Measurements for top & bottom guide line 123 | int beginTopX = 0; 124 | int endTopX = getMeasuredWidth(); 125 | int topY = 0; 126 | 127 | int beginBottomX = 0; 128 | int endBottomX = getMeasuredWidth(); 129 | int bottomY = getMeasuredHeight(); 130 | // Paint the top guide and bottom guide bars 131 | canvas.drawLine(beginTopX, topY, endTopX, topY, mPaintGuides); 132 | canvas.drawLine(beginBottomX, bottomY, endBottomX, bottomY, mPaintGuides); 133 | 134 | // Measurements for pivot indicator 135 | float centerX = calculatePivotXOffset() + getPaddingLeft(); 136 | final int pivotIndicatorLength = getPivotIndicatorLength(); 137 | 138 | // Paint the pivot indicator 139 | canvas.drawLine(centerX, topY + (mPaintWidthPx / 2), centerX, topY + (mPaintWidthPx / 2) + pivotIndicatorLength, mPaintGuides); //line through center of circle 140 | canvas.drawLine(centerX, bottomY - (mPaintWidthPx / 2), centerX, bottomY - (mPaintWidthPx / 2) - pivotIndicatorLength, mPaintGuides); 141 | } 142 | 143 | private int getPivotPadding() { 144 | return getPivotIndicatorLength() * 2 + mAdditonalPadding; 145 | } 146 | 147 | @Override 148 | public void setTextSize(float size) { 149 | super.setTextSize(size); 150 | int pivotPadding = getPivotPadding(); 151 | setPadding(getPaddingLeft(), pivotPadding, getPaddingRight(), pivotPadding); 152 | 153 | } 154 | 155 | private int getPivotIndicatorLength() { 156 | 157 | return getPaint().getFontMetricsInt().bottom; 158 | } 159 | 160 | private float calculatePivotXOffset() { 161 | // Craft a test String of precise length 162 | // to reach pivot character 163 | if (mTestString == null) { 164 | // Spritzer requires monospace font so character is irrelevant 165 | mTestString = "a"; 166 | } 167 | // Measure the rendered distance of CHARS_LEFT_OF_PIVOT chars 168 | // plus half the pivot character 169 | return (getPaint().measureText(mTestString, 0, 1) * (Spritzer.CHARS_LEFT_OF_PIVOT + .50f)); 170 | } 171 | 172 | /** 173 | * This determines the words per minute the sprizter will read at 174 | * 175 | * @param wpm the number of words per minute 176 | */ 177 | public void setWpm(int wpm) { 178 | mSpritzer.setWpm(wpm); 179 | } 180 | 181 | 182 | /** 183 | * Set a custom spritzer 184 | * 185 | * @param spritzer 186 | */ 187 | public void setSpritzer(Spritzer spritzer) { 188 | mSpritzer = spritzer; 189 | mSpritzer.swapTextView(this); 190 | } 191 | 192 | /** 193 | * Pass input text to spritzer object 194 | * 195 | * @param input 196 | */ 197 | public void setSpritzText(String input) { 198 | mSpritzer.setText(input); 199 | } 200 | 201 | /** 202 | * If true, this view will automatically pause or play spritz text upon view clicks 203 | *

204 | * If false, the callback OnClickControls are not invoked and 205 | * 206 | * @param useDefaultClickControls 207 | */ 208 | public void setUseClickControls(boolean useDefaultClickControls) { 209 | mDefaultClickListener = useDefaultClickControls; 210 | } 211 | 212 | /** 213 | * Will play the spritz text that was set in setSpritzText 214 | */ 215 | public void play() { 216 | mSpritzer.start(); 217 | } 218 | 219 | public void pause() { 220 | mSpritzer.pause(); 221 | } 222 | 223 | public int getWpm() { 224 | return mSpritzer.getWpm(); 225 | } 226 | 227 | public void attachProgressBar(ProgressBar bar) { 228 | mSpritzer.attachProgressBar(bar); 229 | 230 | } 231 | 232 | public void setOnCompletionListener(Spritzer.OnCompletionListener listener) { 233 | 234 | mSpritzer.setOnCompletionListener(listener); 235 | } 236 | 237 | /** 238 | * @param strategy @see {@link com.andrewgiang.textspritzer.lib.DelayStrategy#delayMultiplier(String)} 239 | */ 240 | public void setDelayStrategy(DelayStrategy strategy) { 241 | mSpritzer.setDelayStrategy(strategy); 242 | } 243 | 244 | public Spritzer getSpritzer() { 245 | return mSpritzer; 246 | } 247 | 248 | @Override 249 | public void onClick(View v) { 250 | if (mSpritzer.isPlaying()) { 251 | if (mClickControlListener != null) { 252 | mClickControlListener.onPause(); 253 | } 254 | pause(); 255 | } else { 256 | if (mClickControlListener != null) { 257 | mClickControlListener.onPlay(); 258 | } 259 | play(); 260 | } 261 | 262 | } 263 | 264 | public int getCurrentWordIndex() { 265 | return mSpritzer.mCurWordIdx; 266 | } 267 | 268 | public int getMinutesRemainingInQueue() { 269 | return mSpritzer.getMinutesRemainingInQueue(); 270 | } 271 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/andrewgiang/textspritzer/lib/Spritzer.java: -------------------------------------------------------------------------------- 1 | package com.andrewgiang.textspritzer.lib; 2 | 3 | import android.os.Handler; 4 | import android.os.Message; 5 | import android.text.Spannable; 6 | import android.text.SpannableString; 7 | import android.text.style.TextAppearanceSpan; 8 | import android.util.Log; 9 | import android.widget.ProgressBar; 10 | import android.widget.TextView; 11 | 12 | import java.lang.ref.WeakReference; 13 | import java.util.ArrayDeque; 14 | import java.util.Arrays; 15 | 16 | 17 | /** 18 | * Spritzer parses a String into a Queue 19 | * of words, and displays them one-by-one 20 | * onto a TextView at a given WPM. 21 | */ 22 | public class Spritzer { 23 | protected static final String TAG = "Spritzer"; 24 | protected static final boolean VERBOSE = false; 25 | 26 | protected static final int MSG_PRINT_WORD = 1; 27 | 28 | protected static final int MAX_WORD_LENGTH = 13; 29 | protected static final int CHARS_LEFT_OF_PIVOT = 3; 30 | 31 | protected String[] mWordArray; // A parsed list of words parsed from {@link #setText(String input)} 32 | protected ArrayDeque mWordQueue; // The queue of words from mWordArray yet to be displayed 33 | 34 | protected TextView mTarget; 35 | protected int mWPM; 36 | 37 | protected Handler mSpritzHandler; 38 | protected Object mPlayingSync = new Object(); 39 | protected boolean mPlaying; 40 | protected boolean mPlayingRequested; 41 | protected boolean mSpritzThreadStarted; 42 | 43 | protected int mCurWordIdx; 44 | private ProgressBar mProgressBar; 45 | 46 | public interface OnCompletionListener { 47 | 48 | public void onComplete(); 49 | 50 | } 51 | 52 | private DelayStrategy mDelayStrategy; 53 | 54 | public void setOnCompletionListener(OnCompletionListener onCompletionListener) { 55 | mOnCompletionListener = onCompletionListener; 56 | } 57 | 58 | private OnCompletionListener mOnCompletionListener; 59 | 60 | 61 | public Spritzer(TextView target) { 62 | init(); 63 | mTarget = target; 64 | mSpritzHandler = new SpritzHandler(this); 65 | } 66 | 67 | /** 68 | * Prepare to Spritz the given String input 69 | *

70 | * Call {@link #start()} to begin display 71 | * 72 | * @param input 73 | */ 74 | public void setText(String input) { 75 | createWordArrayFromString(input); 76 | setMaxProgress(); 77 | refillWordQueue(); 78 | } 79 | 80 | private void setMaxProgress() { 81 | if (mWordArray != null && mProgressBar != null) { 82 | mProgressBar.setMax(mWordArray.length); 83 | } 84 | } 85 | 86 | private void createWordArrayFromString(String input) { 87 | mWordArray = input 88 | .replaceAll("/\\s+/g", " ") // condense adjacent spaces 89 | .split(" "); // split on spaces 90 | } 91 | 92 | 93 | protected void init() { 94 | 95 | mDelayStrategy = new DefaultDelayStrategy(); 96 | mWordQueue = new ArrayDeque(); 97 | mWPM = 500; 98 | mPlaying = false; 99 | mPlayingRequested = false; 100 | mSpritzThreadStarted = false; 101 | mCurWordIdx = 0; 102 | } 103 | 104 | public int getMinutesRemainingInQueue() { 105 | if (mWordQueue.size() == 0) { 106 | return 0; 107 | } 108 | return mWordQueue.size() / mWPM; 109 | } 110 | 111 | public int getWpm() { 112 | return mWPM; 113 | } 114 | 115 | /** 116 | * Set the target Word Per Minute rate. 117 | * Effective immediately. 118 | * 119 | * @param wpm 120 | */ 121 | public void setWpm(int wpm) { 122 | mWPM = wpm; 123 | } 124 | 125 | /** 126 | * Swap the target TextView. Call this if your 127 | * host Activity is Destroyed and Re-Created. 128 | * Effective immediately. 129 | * 130 | * @param target 131 | */ 132 | public void swapTextView(TextView target) { 133 | mTarget = target; 134 | if (!mPlaying) { 135 | printLastWord(); 136 | } 137 | 138 | } 139 | 140 | /** 141 | * Start displaying the String input 142 | * fed to {@link #setText(String)} 143 | */ 144 | public void start() { 145 | if (mPlaying || mWordArray == null) { 146 | return; 147 | } 148 | if (mWordQueue.isEmpty()) { 149 | refillWordQueue(); 150 | } 151 | 152 | mPlayingRequested = true; 153 | startTimerThread(); 154 | } 155 | 156 | private int getInterWordDelay() { 157 | return 60000 / mWPM; 158 | } 159 | 160 | private void refillWordQueue() { 161 | updateProgress(); 162 | mCurWordIdx = 0; 163 | mWordQueue.clear(); 164 | mWordQueue.addAll(Arrays.asList(mWordArray)); 165 | } 166 | 167 | private void updateProgress() { 168 | if (mProgressBar != null) { 169 | mProgressBar.setProgress(mCurWordIdx); 170 | } 171 | } 172 | 173 | 174 | /** 175 | * Read the current head of mWordQueue and 176 | * submit the appropriate Messages to mSpritzHandler. 177 | *

178 | * Split long words y submitting the first segment of a word 179 | * and placing the second at the head of mWordQueue for processing 180 | * during the next cycle. 181 | *

182 | * Must be called on a background thread, as this method uses 183 | * {@link Thread#sleep(long)} to time pauses in display. 184 | * 185 | * @throws InterruptedException 186 | */ 187 | protected void processNextWord() throws InterruptedException { 188 | if (!mWordQueue.isEmpty()) { 189 | String word = mWordQueue.remove(); 190 | mCurWordIdx += 1; 191 | // Split long words, at hyphen if present 192 | word = splitLongWord(word); 193 | 194 | mSpritzHandler.sendMessage(mSpritzHandler.obtainMessage(MSG_PRINT_WORD, word)); 195 | 196 | final int delayMultiplier = mDelayStrategy.delayMultiplier(word); 197 | //Do not allow multiplier that is less than 1 198 | final int wordDelay = getInterWordDelay() * (mDelayStrategy != null ? delayMultiplier < 1 ? 1 : delayMultiplier : 1); 199 | Thread.sleep(wordDelay); 200 | 201 | } 202 | updateProgress(); 203 | } 204 | 205 | /** 206 | * Split the given String if appropriate and 207 | * add the tail of the split to the head of 208 | * {@link #mWordQueue} 209 | * 210 | * @param word 211 | * @return 212 | */ 213 | protected String splitLongWord(String word) { 214 | if (word.length() > MAX_WORD_LENGTH) { 215 | int splitIndex = findSplitIndex(word); 216 | String firstSegment; 217 | if (VERBOSE) { 218 | Log.i(TAG, "Splitting long word " + word + " into " + word.substring(0, splitIndex) + " and " + word.substring(splitIndex)); 219 | } 220 | firstSegment = word.substring(0, splitIndex); 221 | // A word split is always indicated with a hyphen unless ending in a period 222 | if (!firstSegment.contains("-") && !firstSegment.endsWith(".")) { 223 | firstSegment = firstSegment + "-"; 224 | } 225 | mCurWordIdx--; //have to account for the added word in the queue 226 | mWordQueue.addFirst(word.substring(splitIndex)); 227 | word = firstSegment; 228 | 229 | } 230 | return word; 231 | } 232 | 233 | /** 234 | * Determine the split index on a given String 235 | * e.g If it exceeds MAX_WORD_LENGTH or contains a hyphen 236 | * 237 | * @param thisWord 238 | * @return the index on which to split the given String 239 | */ 240 | private int findSplitIndex(String thisWord) { 241 | int splitIndex; 242 | // Split long words, at hyphen or dot if present. 243 | if (thisWord.contains("-")) { 244 | splitIndex = thisWord.indexOf("-") + 1; 245 | } else if (thisWord.contains(".")) { 246 | splitIndex = thisWord.indexOf(".") + 1; 247 | } else if (thisWord.length() > MAX_WORD_LENGTH * 2) { 248 | // if the word is floccinaucinihilipilifcation, for example. 249 | splitIndex = MAX_WORD_LENGTH - 1; 250 | // 12 characters plus a "-" == 13. 251 | } else { 252 | // otherwise we want to split near the middle. 253 | splitIndex = Math.round(thisWord.length() / 2F); 254 | } 255 | // in case we found a split character that was > MAX_WORD_LENGTH characters in. 256 | if (splitIndex > MAX_WORD_LENGTH) { 257 | // If we split the word at a splitting char like "-" or ".", we added one to the splitIndex 258 | // in order to ensure the splitting char appears at the head of the split. Not accounting 259 | // for this in the recursive call will cause a StackOverflowException 260 | return findSplitIndex(thisWord.substring(0, 261 | wordContainsSplittingCharacter(thisWord) ? splitIndex - 1 : splitIndex)); 262 | } 263 | if (VERBOSE) { 264 | Log.i(TAG, "Splitting long word " + thisWord + " into " + thisWord.substring(0, splitIndex) + 265 | " and " + thisWord.substring(splitIndex)); 266 | } 267 | return splitIndex; 268 | } 269 | 270 | private boolean wordContainsSplittingCharacter(String word) { 271 | return (word.contains(".") || word.contains("-")); 272 | } 273 | 274 | 275 | private void printLastWord() { 276 | if (mWordArray != null) { 277 | printWord(mWordArray[mWordArray.length - 1]); 278 | } 279 | } 280 | 281 | /** 282 | * Applies the given String to this Spritzer's TextView, 283 | * padding the beginning if necessary to align the pivot character. 284 | * Styles the pivot character. 285 | * 286 | * @param word 287 | */ 288 | private void printWord(String word) { 289 | int startSpan = 0; 290 | int endSpan = 0; 291 | word = word.trim(); 292 | if (VERBOSE) Log.i(TAG + word.length(), word); 293 | if (word.length() == 1) { 294 | StringBuilder builder = new StringBuilder(); 295 | for (int x = 0; x < CHARS_LEFT_OF_PIVOT; x++) { 296 | builder.append(" "); 297 | } 298 | builder.append(word); 299 | word = builder.toString(); 300 | startSpan = CHARS_LEFT_OF_PIVOT; 301 | endSpan = startSpan + 1; 302 | } else if (word.length() <= CHARS_LEFT_OF_PIVOT * 2) { 303 | StringBuilder builder = new StringBuilder(); 304 | int halfPoint = word.length() / 2; 305 | int beginPad = CHARS_LEFT_OF_PIVOT - halfPoint; 306 | for (int x = 0; x <= beginPad; x++) { 307 | builder.append(" "); 308 | } 309 | builder.append(word); 310 | word = builder.toString(); 311 | startSpan = halfPoint + beginPad; 312 | endSpan = startSpan + 1; 313 | if (VERBOSE) Log.i(TAG + word.length(), "pivot: " + word.substring(startSpan, endSpan)); 314 | } else { 315 | startSpan = CHARS_LEFT_OF_PIVOT; 316 | endSpan = startSpan + 1; 317 | } 318 | 319 | Spannable spanRange = new SpannableString(word); 320 | TextAppearanceSpan tas = new TextAppearanceSpan(mTarget.getContext(), R.style.PivotLetter); 321 | spanRange.setSpan(tas, startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 322 | mTarget.setText(spanRange); 323 | } 324 | 325 | public void pause() { 326 | mPlayingRequested = false; 327 | } 328 | 329 | public boolean isPlaying() { 330 | return mPlaying; 331 | } 332 | 333 | /** 334 | * Begin the background timer thread 335 | */ 336 | private void startTimerThread() { 337 | synchronized (mPlayingSync) { 338 | if (!mSpritzThreadStarted) { 339 | new Thread(new Runnable() { 340 | @Override 341 | public void run() { 342 | if (VERBOSE) { 343 | Log.i(TAG, "Starting spritzThread with queue length " + mWordQueue.size()); 344 | } 345 | mPlaying = true; 346 | mSpritzThreadStarted = true; 347 | while (mPlayingRequested) { 348 | try { 349 | processNextWord(); 350 | if (mWordQueue.isEmpty()) { 351 | if (VERBOSE) { 352 | Log.i(TAG, "Queue is empty after processNextWord. Pausing"); 353 | } 354 | mTarget.post(new Runnable() { 355 | @Override 356 | public void run() { 357 | if (mOnCompletionListener != null) { 358 | mOnCompletionListener.onComplete(); 359 | } 360 | } 361 | }); 362 | mPlayingRequested = false; 363 | 364 | } 365 | } catch (InterruptedException e) { 366 | e.printStackTrace(); 367 | } 368 | } 369 | 370 | 371 | if (VERBOSE) 372 | Log.i(TAG, "Stopping spritzThread"); 373 | mPlaying = false; 374 | mSpritzThreadStarted = false; 375 | 376 | } 377 | }).start(); 378 | } 379 | } 380 | } 381 | 382 | 383 | public String[] getWordArray() { 384 | return mWordArray; 385 | } 386 | 387 | public ArrayDeque getWordQueue() { 388 | return mWordQueue; 389 | } 390 | 391 | public void attachProgressBar(ProgressBar bar) { 392 | if (bar != null) { 393 | mProgressBar = bar; 394 | } 395 | } 396 | 397 | 398 | /** 399 | * @param strategy @see{@link com.andrewgiang.textspritzer.lib.DelayStrategy#delayMultiplier(String) } 400 | */ 401 | public void setDelayStrategy(DelayStrategy strategy) { 402 | mDelayStrategy = strategy; 403 | 404 | } 405 | 406 | /** 407 | * A Handler intended for creation on the Main thread. 408 | * Messages are intended to be passed from a background 409 | * timing thread. This Handler communicates timing 410 | * thread events to the Main thread for UI update. 411 | */ 412 | protected static class SpritzHandler extends Handler { 413 | private WeakReference mWeakSpritzer; 414 | 415 | public SpritzHandler(Spritzer muxer) { 416 | mWeakSpritzer = new WeakReference(muxer); 417 | } 418 | 419 | @Override 420 | public void handleMessage(Message inputMessage) { 421 | int what = inputMessage.what; 422 | Object obj = inputMessage.obj; 423 | 424 | Spritzer spritzer = mWeakSpritzer.get(); 425 | if (spritzer == null) { 426 | return; 427 | } 428 | 429 | switch (what) { 430 | case MSG_PRINT_WORD: 431 | spritzer.printWord((String) obj); 432 | break; 433 | default: 434 | throw new RuntimeException("Unexpected msg what=" + what); 435 | } 436 | } 437 | 438 | } 439 | 440 | 441 | } --------------------------------------------------------------------------------