├── 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 | [](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 | 
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 | [](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 | }
--------------------------------------------------------------------------------