├── .gitignore ├── License.txt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── proguard-rules.pro ├── pulse-indicator-sample ├── build.gradle ├── proguard-rules.pro ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── jackpocket │ │ └── pulse │ │ └── test │ │ └── MainActivity.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── base__location.xml │ └── ic_launcher_background.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── pulse-indicator ├── build.gradle ├── publishing.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── jackpocket │ │ └── pulse │ │ ├── Pulse.java │ │ ├── PulseController.java │ │ ├── PulseTask.java │ │ └── layouts │ │ ├── PulseLayout.java │ │ ├── PulsingLinearLayout.java │ │ ├── PulsingRelativeLayout.java │ │ └── PulsingView.java │ └── res │ └── values │ ├── attrs.xml │ ├── booleans.xml │ ├── colors.xml │ ├── dimens.xml │ ├── integers.xml │ ├── strings.xml │ └── styles.xml ├── pulse.gif ├── readme.md └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | build/ 7 | /captures 8 | .idea/* 9 | *.iml 10 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 Jackpocket Inc 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | targetSdkVersion = 30 4 | buildToolsVersion = "30.0.3" 5 | kotlinVersion = "1.5.21" 6 | } 7 | 8 | repositories { 9 | maven { url "https://maven.google.com/" } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | 14 | dependencies { 15 | classpath "com.android.tools.build:gradle:4.2.2" 16 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | maven { url "https://maven.google.com/" } 23 | mavenCentral() 24 | gradlePluginPortal() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackpocket/android-pulse-indicator/a2a8363ded4699ad22e1fd6964111c17d44fe75f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Oct 21 11:34:03 PDT 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip 7 | -------------------------------------------------------------------------------- /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 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/mattsilber/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /pulse-indicator-sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion rootProject.ext.targetSdkVersion 6 | buildToolsVersion rootProject.ext.buildToolsVersion 7 | 8 | defaultConfig { 9 | applicationId "com.jackpocket.pulse.test" 10 | 11 | minSdkVersion 14 12 | targetSdkVersion rootProject.ext.targetSdkVersion 13 | 14 | versionCode 1 15 | versionName "1.0.0" 16 | 17 | vectorDrawables.useSupportLibrary = true 18 | 19 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 20 | } 21 | } 22 | 23 | dependencies { 24 | implementation project(path: ':pulse-indicator') 25 | 26 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" 27 | implementation "androidx.appcompat:appcompat:1.1.0" 28 | implementation "androidx.constraintlayout:constraintlayout:1.1.2" 29 | 30 | testImplementation "junit:junit:4.12" 31 | } 32 | -------------------------------------------------------------------------------- /pulse-indicator-sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /pulse-indicator-sample/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':pulse-indicator' 2 | -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/java/com/jackpocket/pulse/test/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.jackpocket.pulse.test 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.view.View 6 | import android.view.animation.AccelerateInterpolator 7 | import android.view.animation.LinearInterpolator 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.appcompat.widget.AppCompatImageView 10 | import com.jackpocket.pulse.PulseController 11 | import com.jackpocket.pulse.layouts.PulsingLinearLayout 12 | 13 | class MainActivity: AppCompatActivity(), PulseController.PulseEventListener { 14 | 15 | private val pulseLayout: PulsingLinearLayout 16 | get() = findViewById(R.id.main__pulse_layout) 17 | 18 | private val pulseTarget: AppCompatImageView 19 | get() = findViewById(R.id.main__pulse_target_image) 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | 24 | setContentView(R.layout.activity_main) 25 | } 26 | 27 | fun onTestPulseClicked(view: View?) { 28 | Log.d(TAG, "Pulse starting...") 29 | 30 | pulseLayout.pulseController 31 | .setCirclePathOverride(true) 32 | .setPulsingColor(0x22FF22) 33 | .setPulsingStrokeWidth(10) 34 | .setDurationMs(1500) 35 | .setPulseLifeSpanMs(900) 36 | .setRespawnRateMs(300) 37 | .setAlphaInterpolator(AccelerateInterpolator()) 38 | .setScaleInterpolator(LinearInterpolator()) 39 | .setFinishedListener(this) 40 | .attachTo(this, pulseTarget) 41 | } 42 | 43 | override fun onPulseEvent(target: View?) { 44 | Log.d(TAG, "Pulse completed.") 45 | } 46 | 47 | fun onStopPulseClicked(view: View?) { 48 | Log.d(TAG, "Stopping pulse...") 49 | 50 | pulseLayout.pulseController.stopPulsing() 51 | } 52 | 53 | fun onSuspendPulseClicked(view: View?) { 54 | Log.d(TAG, "Stopping pulse...") 55 | 56 | pulseLayout.pulseController.suspendPulseCreation() 57 | } 58 | 59 | companion object { 60 | 61 | const val TAG = "PulseIndicatorTest" 62 | } 63 | } -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/drawable/base__location.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 19 | 20 | 26 | 27 | 37 | 38 | 41 | 42 | 52 | 53 | 56 | 57 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackpocket/android-pulse-indicator/a2a8363ded4699ad22e1fd6964111c17d44fe75f/pulse-indicator-sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackpocket/android-pulse-indicator/a2a8363ded4699ad22e1fd6964111c17d44fe75f/pulse-indicator-sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackpocket/android-pulse-indicator/a2a8363ded4699ad22e1fd6964111c17d44fe75f/pulse-indicator-sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackpocket/android-pulse-indicator/a2a8363ded4699ad22e1fd6964111c17d44fe75f/pulse-indicator-sample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackpocket/android-pulse-indicator/a2a8363ded4699ad22e1fd6964111c17d44fe75f/pulse-indicator-sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackpocket/android-pulse-indicator/a2a8363ded4699ad22e1fd6964111c17d44fe75f/pulse-indicator-sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackpocket/android-pulse-indicator/a2a8363ded4699ad22e1fd6964111c17d44fe75f/pulse-indicator-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackpocket/android-pulse-indicator/a2a8363ded4699ad22e1fd6964111c17d44fe75f/pulse-indicator-sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackpocket/android-pulse-indicator/a2a8363ded4699ad22e1fd6964111c17d44fe75f/pulse-indicator-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackpocket/android-pulse-indicator/a2a8363ded4699ad22e1fd6964111c17d44fe75f/pulse-indicator-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | pulse-indicator-test 3 | 4 | -------------------------------------------------------------------------------- /pulse-indicator-sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /pulse-indicator/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.targetSdkVersion 5 | buildToolsVersion rootProject.ext.buildToolsVersion 6 | 7 | defaultConfig { 8 | minSdkVersion 9 9 | targetSdkVersion rootProject.ext.targetSdkVersion 10 | } 11 | } 12 | 13 | apply from: "./publishing.gradle" -------------------------------------------------------------------------------- /pulse-indicator/publishing.gradle: -------------------------------------------------------------------------------- 1 | def localPropertiesFile = new File(project.rootProject.getRootDir(), 'local.properties') 2 | 3 | if (!localPropertiesFile.exists()) { 4 | return 5 | } 6 | 7 | Properties localProperties = new Properties() 8 | localProperties.load(localPropertiesFile.newDataInputStream()) 9 | 10 | if (localProperties.getProperty("release_mode", "false") != "true") { 11 | return 12 | } 13 | 14 | apply plugin: 'maven-publish' 15 | apply plugin: 'signing' 16 | 17 | ext { 18 | mavPublishGroupId = "com.jackpocket" 19 | mavProjectName = "pulse-indicator" 20 | mavLibraryDescription = "An Android system for indicating Views with fading pulses" 21 | mavLibraryVersion = "2.0.0" 22 | 23 | mavLicenseName = "The Apache License, Version 2.0" 24 | mavLicenseUrl = "http://www.apache.org/licenses/LICENSE-2.0.txt" 25 | 26 | mavDeveloperId = "jackpocket" 27 | mavDeveloperName = "Jackpocket" 28 | mavDeveloperEmail = "services@jackpocket.com" 29 | mavDeveloperOrg = "Jackpocket" 30 | mavDeveloperOrgUrl = "https://jackpocket.com" 31 | 32 | mavSiteUrl = "https://github.com/jackpocket/pulse-indicator" 33 | mavGitUrl = "https://github.com/jackpocket/pulse-indicator.git" 34 | mavScmConnection = "scm:git:git://github.com:jackpocket/pulse-indicator.git" 35 | mavScmDeveloperConnection = "scm:git:ssh://github.com:jackpocket/pulse-indicator.git" 36 | 37 | mavDeploymentRepo = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" 38 | 39 | mavenCentralUsername = localProperties.getProperty("mavenCentralUsername") 40 | mavenCentralPassword = localProperties.getProperty("mavenCentralPassword") 41 | } 42 | 43 | ext["signing.keyId"] = localProperties.getProperty("signing.keyId") 44 | ext["signing.password"] = localProperties.getProperty("signing.password") 45 | ext["signing.secretKeyRingFile"] = localProperties.getProperty("signing.secretKeyRingFile") 46 | 47 | task sourcesJar(type: Jar) { 48 | archiveClassifier.set('sources') 49 | from android.sourceSets.main.java.srcDirs 50 | } 51 | 52 | task javadoc(type: Javadoc) { 53 | source = android.sourceSets.main.java.srcDirs 54 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 55 | 56 | android.libraryVariants.all { variant -> 57 | if (variant.name == 'release') { 58 | owner.classpath += variant.javaCompileProvider.get().classpath 59 | } 60 | } 61 | } 62 | 63 | task javadocJar(type: Jar, dependsOn: javadoc) { 64 | archiveClassifier.set('javadoc') 65 | from javadoc.destinationDir 66 | } 67 | 68 | afterEvaluate { 69 | publishing { 70 | publications { 71 | release(MavenPublication) { 72 | from components.release 73 | 74 | artifact sourcesJar 75 | artifact javadocJar 76 | 77 | groupId = mavPublishGroupId 78 | artifactId = mavProjectName 79 | version = mavLibraryVersion 80 | 81 | pom { 82 | name = mavProjectName 83 | description = mavLibraryDescription 84 | url = mavSiteUrl 85 | licenses { 86 | license { 87 | name = mavLicenseName 88 | url = mavLicenseUrl 89 | } 90 | } 91 | developers { 92 | developer { 93 | id = mavDeveloperId 94 | name = mavDeveloperName 95 | email = mavDeveloperEmail 96 | organization = mavDeveloperOrg 97 | organizationUrl = mavDeveloperOrgUrl 98 | } 99 | } 100 | scm { 101 | connection = mavScmConnection 102 | developerConnection = mavScmDeveloperConnection 103 | url = mavGitUrl 104 | } 105 | } 106 | } 107 | } 108 | 109 | repositories { 110 | maven { 111 | name = 'mavenCentral' 112 | url = mavDeploymentRepo 113 | 114 | credentials { 115 | username mavenCentralUsername 116 | password mavenCentralPassword 117 | } 118 | } 119 | } 120 | } 121 | 122 | signing { 123 | sign publishing.publications 124 | } 125 | } -------------------------------------------------------------------------------- /pulse-indicator/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/java/com/jackpocket/pulse/Pulse.java: -------------------------------------------------------------------------------- 1 | package com.jackpocket.pulse; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.CornerPathEffect; 5 | import android.graphics.Paint; 6 | import android.graphics.Path; 7 | import android.graphics.Rect; 8 | import android.view.animation.Interpolator; 9 | 10 | public class Pulse { 11 | 12 | private static final int MAX_ALPHA = 255; 13 | 14 | protected Paint paint; 15 | protected Interpolator alphaInterpolator; 16 | protected Interpolator scaleInterpolator; 17 | 18 | protected Rect startBoundaries; 19 | protected Path path; 20 | 21 | protected boolean circlePathOverride = false; 22 | 23 | protected float maxScale = 10f; 24 | protected float scale = 1f; 25 | 26 | protected long createdAt = 0; 27 | protected long duration = 1000; 28 | 29 | protected int[] centers; 30 | protected int radius = 0; 31 | 32 | public Pulse(Rect startBoundaries){ 33 | this(startBoundaries, true); 34 | } 35 | 36 | public Pulse(Rect startBoundaries, boolean circlePathOverride) { 37 | this.startBoundaries = startBoundaries; 38 | this.circlePathOverride = circlePathOverride; 39 | this.paint = buildPaint(); 40 | 41 | this.centers = new int[] { 42 | startBoundaries.left + ((startBoundaries.right - startBoundaries.left) / 2), 43 | Math.abs(startBoundaries.top + ((startBoundaries.bottom - startBoundaries.top) / 2)) 44 | }; 45 | 46 | this.path = buildPath(); 47 | this.createdAt = System.currentTimeMillis(); 48 | } 49 | 50 | protected Paint buildPaint() { 51 | Paint paint = new Paint(); 52 | paint.setAntiAlias(true); 53 | paint.setDither(true); 54 | paint.setStyle(Paint.Style.STROKE); 55 | paint.setStrokeJoin(Paint.Join.ROUND); 56 | paint.setStrokeCap(Paint.Cap.ROUND); 57 | paint.setPathEffect(new CornerPathEffect(10)); 58 | 59 | return paint; 60 | } 61 | 62 | protected Path buildPath(){ 63 | Path path = new Path(); 64 | 65 | if(circlePathOverride) { 66 | int horizontal = startBoundaries.right - startBoundaries.left; 67 | int vertical = Math.abs(startBoundaries.bottom - startBoundaries.top); 68 | 69 | this.radius = Math.min(horizontal, vertical) / 2; 70 | 71 | path.addCircle(centers[0], centers[1], radius, Path.Direction.CW); 72 | } 73 | else { 74 | path.moveTo(startBoundaries.left, startBoundaries.top); 75 | path.lineTo(startBoundaries.right, startBoundaries.top); 76 | path.lineTo(startBoundaries.right, startBoundaries.bottom); 77 | path.lineTo(startBoundaries.left, startBoundaries.bottom); 78 | path.lineTo(startBoundaries.left, startBoundaries.top); 79 | } 80 | 81 | return path; 82 | } 83 | 84 | public void draw(Canvas canvas) { 85 | canvas.save(); 86 | canvas.scale(scale, scale, centers[0], centers[1]); 87 | canvas.drawPath(path, paint); 88 | canvas.restore(); 89 | } 90 | 91 | public void update() { 92 | float percentCompleted = (System.currentTimeMillis() - createdAt) / (float) duration; 93 | 94 | if(alphaInterpolator != null) { 95 | this.paint.setAlpha((int) (MAX_ALPHA - (alphaInterpolator.getInterpolation(percentCompleted) * MAX_ALPHA))); 96 | } 97 | 98 | if(scaleInterpolator != null) { 99 | this.scale = 1 + ((maxScale - 1) * scaleInterpolator.getInterpolation(percentCompleted)); 100 | } 101 | } 102 | 103 | public Pulse setPaint(Paint paint) { 104 | this.paint = paint; 105 | 106 | return this; 107 | } 108 | 109 | public Pulse setAlphaInterpolator(Interpolator alphaInterpolator) { 110 | this.alphaInterpolator = alphaInterpolator; 111 | 112 | return this; 113 | } 114 | 115 | public Pulse setScaleInterpolator(Interpolator scaleInterpolator) { 116 | this.scaleInterpolator = scaleInterpolator; 117 | 118 | return this; 119 | } 120 | 121 | public Pulse setDuration(long duration) { 122 | this.duration = duration; 123 | 124 | return this; 125 | } 126 | 127 | public Pulse setCirclePathOverride(boolean circlePathOverride) { 128 | this.circlePathOverride = circlePathOverride; 129 | 130 | return this; 131 | } 132 | 133 | public Pulse setMaxScale(float maxScale) { 134 | this.maxScale = maxScale; 135 | 136 | return this; 137 | } 138 | 139 | public Pulse setColor(int color){ 140 | paint.setColor(color); 141 | 142 | return this; 143 | } 144 | 145 | public Pulse setStrokeWidth(int strokeWidth){ 146 | paint.setStrokeWidth(strokeWidth); 147 | 148 | return this; 149 | } 150 | 151 | public boolean isAlive(){ 152 | return System.currentTimeMillis() - createdAt < duration; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/java/com/jackpocket/pulse/PulseController.java: -------------------------------------------------------------------------------- 1 | package com.jackpocket.pulse; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.Rect; 7 | import android.view.View; 8 | import android.view.animation.AccelerateInterpolator; 9 | import android.view.animation.Interpolator; 10 | import android.view.animation.LinearInterpolator; 11 | 12 | import java.lang.ref.WeakReference; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | public class PulseController { 18 | 19 | public interface PulseEventListener { 20 | public void onPulseEvent(View target); 21 | } 22 | 23 | protected WeakReference parent; 24 | 25 | protected WeakReference pulseTarget = new WeakReference(null); 26 | protected Bitmap pulseTargetDrawingCache; 27 | protected Rect pulseStartBoundaries = new Rect(); 28 | 29 | protected final ArrayList pulses = new ArrayList(); 30 | 31 | protected Interpolator alphaInterpolator = new AccelerateInterpolator(); 32 | protected Interpolator scaleInterpolator = new LinearInterpolator(); 33 | 34 | protected long durationMs = 1500; 35 | protected long pulseLifeSpanMs = 900; 36 | protected long respawnRateMs = 300; 37 | protected boolean respawnAllowed = true; 38 | 39 | protected float pulseMaxScale = 3; 40 | 41 | protected long startTimeMs = 0; 42 | protected long lastAddedMs = 0; 43 | protected boolean circlePathOverride = true; 44 | 45 | protected int pulsingColor; 46 | 47 | protected int pulsingStrokeWidth = -1; 48 | protected int defaultPulsingStrokeWidth = 1; 49 | 50 | protected PulseTask pulseTask; 51 | 52 | protected WeakReference finishedListener; 53 | 54 | private final Object lock = new Object(); 55 | 56 | /** 57 | * @param parent the non-null View triggering the controller's drawing (i.e. the PulseLayout) 58 | */ 59 | public PulseController(View parent) { 60 | if (pulseTarget == null) 61 | throw new RuntimeException("View supplied to PulseController() cannot be null!"); 62 | 63 | this.parent = new WeakReference(parent); 64 | 65 | this.circlePathOverride = parent.getContext() 66 | .getResources() 67 | .getBoolean(R.bool.pulse__circle_path_default); 68 | 69 | this.pulseMaxScale = parent.getContext() 70 | .getResources() 71 | .getInteger(R.integer.pulse__max_scale_percent_default) / 100f; 72 | 73 | this.durationMs = parent.getContext() 74 | .getResources() 75 | .getInteger(R.integer.pulse__duration_default); 76 | 77 | this.pulseLifeSpanMs = parent.getContext() 78 | .getResources() 79 | .getInteger(R.integer.pulse__lifespan_default); 80 | 81 | this.respawnRateMs = parent.getContext() 82 | .getResources() 83 | .getInteger(R.integer.pulse__respawn_rate_default); 84 | 85 | this.pulsingColor = parent.getContext() 86 | .getResources() 87 | .getColor(R.color.pulse__color); 88 | } 89 | 90 | /** 91 | * Post {@link PulseController#attachTo(Activity,View)} call on the UI Thread. 92 | * 93 | * @param activity 94 | * @param pulseTarget the non-null target to pulse behind 95 | */ 96 | public PulseController attachToOnUiThread(Activity activity, View pulseTarget) { 97 | if (pulseTarget == null) 98 | throw new RuntimeException("View supplied to PulseController.attachToOnUiThread cannot be null!"); 99 | 100 | final WeakReference weakActivity = new WeakReference(activity); 101 | final WeakReference weakTarget = new WeakReference(pulseTarget); 102 | 103 | Runnable attachmentRunnable = new Runnable() { 104 | @Override 105 | public void run() { 106 | Activity activity = weakActivity.get(); 107 | View target = weakTarget.get(); 108 | 109 | if (activity == null || target == null) 110 | return; 111 | 112 | PulseController.this.attachTo(activity, target); 113 | } 114 | }; 115 | 116 | activity.runOnUiThread(attachmentRunnable); 117 | 118 | return this; 119 | } 120 | 121 | /** 122 | * Attach to the target and begin the pulsing sequence. 123 | * 124 | * @param activity 125 | * @param pulseTarget the non-null target to pulse behind 126 | */ 127 | public PulseController attachTo(Activity activity, View pulseTarget) { 128 | if (pulseTarget == null) 129 | throw new RuntimeException("View supplied to PulseController.attachTo cannot be null!"); 130 | 131 | this.pulseTarget = new WeakReference(pulseTarget); 132 | this.pulseStartBoundaries = findViewInParent(activity, pulseTarget); 133 | this.pulseTargetDrawingCache = getDrawingCache(pulseTarget); 134 | this.defaultPulsingStrokeWidth = (int) Math.max(5, Math.abs((pulseStartBoundaries.right - pulseStartBoundaries.left)) * .065); 135 | this.startTimeMs = System.currentTimeMillis(); 136 | this.respawnAllowed = true; 137 | 138 | cancelPulseTask(); 139 | 140 | this.pulseTask = new PulseTask(this) 141 | .setFinishedListener(new Runnable() { 142 | public void run() { 143 | finishPulsing(); 144 | } 145 | }); 146 | 147 | this.pulseTask.start(); 148 | 149 | return this; 150 | } 151 | 152 | protected Rect findViewInParent(Activity activity, View view) { 153 | Rect viewRect = getWindowLocation(activity, view); 154 | 155 | stripParentPositions(activity, viewRect); 156 | 157 | return viewRect; 158 | } 159 | 160 | protected void stripParentPositions(Activity activity, Rect rect) { 161 | Rect parentRect = getWindowLocation(activity, this.parent.get()); 162 | 163 | rect.left = rect.left - parentRect.left; 164 | rect.top = rect.top - parentRect.top; 165 | rect.right = rect.right - parentRect.left; 166 | rect.bottom = rect.bottom - parentRect.top; 167 | } 168 | 169 | protected Rect getWindowLocation(Activity activity, View view) { 170 | int[] windowLocation = new int[2]; 171 | 172 | Rect statusBar = new Rect(); 173 | 174 | activity.getWindow() 175 | .getDecorView() 176 | .getWindowVisibleDisplayFrame(statusBar); 177 | 178 | view.getLocationInWindow(windowLocation); 179 | 180 | windowLocation[1] = windowLocation[1] - statusBar.height(); // Stupid statusbar ruins the positioning 181 | 182 | Rect rect = new Rect(); 183 | rect.left = windowLocation[0]; 184 | rect.top = windowLocation[1]; 185 | rect.right = windowLocation[0] + view.getWidth(); 186 | rect.bottom = windowLocation[1] + view.getHeight(); 187 | 188 | return rect; 189 | } 190 | 191 | public void draw(Canvas canvas) { 192 | List pulsesToDraw; 193 | 194 | synchronized (lock) { 195 | pulsesToDraw = new ArrayList(pulses); 196 | } 197 | 198 | for (Pulse pulse : pulsesToDraw) { 199 | pulse.draw(canvas); 200 | } 201 | 202 | if (pulseTargetDrawingCache == null) 203 | return; 204 | 205 | canvas.drawBitmap( 206 | pulseTargetDrawingCache, 207 | pulseStartBoundaries.left, 208 | pulseStartBoundaries.top, 209 | null); 210 | } 211 | 212 | protected Bitmap getDrawingCache(View view) { 213 | view.setDrawingCacheEnabled(true); 214 | view.buildDrawingCache(); 215 | 216 | Bitmap bitmap = view.getDrawingCache(); 217 | 218 | if (bitmap != null) { 219 | bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight()); 220 | } 221 | 222 | view.setDrawingCacheEnabled(false); 223 | 224 | return bitmap; 225 | } 226 | 227 | public void update() { 228 | if (!isRunning()) 229 | return; 230 | 231 | addNewPulseIfPossible(); 232 | 233 | synchronized (lock) { 234 | for (Pulse pulse : pulses) { 235 | pulse.update(); 236 | } 237 | 238 | for (int i = pulses.size() - 1; 0 <= i; i--) { 239 | if (pulses.get(i).isAlive()) 240 | continue; 241 | 242 | pulses.remove(i); 243 | } 244 | } 245 | 246 | safelyInvalidateParent(); 247 | } 248 | 249 | protected void addNewPulseIfPossible() { 250 | synchronized (lock) { 251 | if (isPulseAddingAvailable()) { 252 | this.lastAddedMs = System.currentTimeMillis(); 253 | this.pulses.add(buildPulse()); 254 | } 255 | } 256 | } 257 | 258 | protected Pulse buildPulse() { 259 | return new Pulse(pulseStartBoundaries, circlePathOverride) 260 | .setColor(pulsingColor) 261 | .setStrokeWidth(pulsingStrokeWidth < 1 ? defaultPulsingStrokeWidth : pulsingStrokeWidth) 262 | .setAlphaInterpolator(alphaInterpolator) 263 | .setScaleInterpolator(scaleInterpolator) 264 | .setDuration(pulseLifeSpanMs) 265 | .setMaxScale(pulseMaxScale); 266 | } 267 | 268 | protected void safelyInvalidateParent() { 269 | View parent = this.parent.get(); 270 | 271 | if (parent == null) 272 | return; 273 | 274 | parent.invalidate(); 275 | } 276 | 277 | public boolean isRunning() { 278 | synchronized (lock) { 279 | return System.currentTimeMillis() - startTimeMs < durationMs 280 | || 0 < pulses.size(); 281 | } 282 | } 283 | 284 | protected boolean isPulseAddingAvailable() { 285 | return respawnAllowed 286 | && System.currentTimeMillis() - startTimeMs < durationMs 287 | && respawnRateMs < System.currentTimeMillis() - lastAddedMs; 288 | } 289 | 290 | protected void finishPulsing() { 291 | View pulseTarget = this.pulseTarget.get(); 292 | 293 | stopPulsing(); 294 | 295 | final PulseEventListener completionCallback = this.finishedListener.get(); 296 | 297 | if (completionCallback != null) { 298 | completionCallback.onPulseEvent(pulseTarget); 299 | } 300 | } 301 | 302 | /** 303 | * Immediately stop all current and new Pulses from being created. 304 | *

305 | * Completion callbacks will not be triggered. 306 | */ 307 | public PulseController stopPulsing() { 308 | cancelPulseTask(); 309 | 310 | synchronized (lock) { 311 | this.pulses.clear(); 312 | } 313 | 314 | this.pulseTarget = new WeakReference(null); 315 | 316 | safelyInvalidateParent(); 317 | 318 | return this; 319 | } 320 | 321 | protected void cancelPulseTask() { 322 | if (pulseTask == null) 323 | return; 324 | 325 | this.pulseTask.cancel(); 326 | this.pulseTask = null; 327 | } 328 | 329 | /** 330 | * Suspend the creation of new animated Pulses. This will continue 331 | * currently-active Pulses until all have been completed, then 332 | * finish normally. 333 | */ 334 | public PulseController suspendPulseCreation() { 335 | this.respawnAllowed = false; 336 | 337 | return this; 338 | } 339 | 340 | public PulseController setAlphaInterpolator(Interpolator alphaInterpolator) { 341 | this.alphaInterpolator = alphaInterpolator; 342 | 343 | return this; 344 | } 345 | 346 | public PulseController setScaleInterpolator(Interpolator scaleInterpolator) { 347 | this.scaleInterpolator = scaleInterpolator; 348 | 349 | return this; 350 | } 351 | 352 | public PulseController setDuration(long duration, TimeUnit unit) { 353 | return setDurationMs(unit.toMillis(duration)); 354 | } 355 | 356 | public PulseController setDurationMs(long durationMs) { 357 | this.durationMs = durationMs; 358 | 359 | return this; 360 | } 361 | 362 | public PulseController setPulseLifeSpan(long pulseLifeSpan, TimeUnit unit) { 363 | return setPulseLifeSpanMs(unit.toMillis(pulseLifeSpan)); 364 | } 365 | 366 | public PulseController setPulseLifeSpanMs(long pulseLifeSpanMs) { 367 | this.pulseLifeSpanMs = pulseLifeSpanMs; 368 | 369 | return this; 370 | } 371 | 372 | public PulseController setRespawnRate(long respawnRateMs, TimeUnit unit) { 373 | return setRespawnRateMs(unit.toMillis(respawnRateMs)); 374 | } 375 | 376 | public PulseController setRespawnRateMs(long respawnRateMs) { 377 | this.respawnRateMs = respawnRateMs; 378 | 379 | return this; 380 | } 381 | 382 | public PulseController setPulseMaxScale(float pulseMaxScale) { 383 | this.pulseMaxScale = pulseMaxScale; 384 | 385 | return this; 386 | } 387 | 388 | public PulseController setCirclePathOverride(boolean circlePathOverride) { 389 | this.circlePathOverride = circlePathOverride; 390 | 391 | return this; 392 | } 393 | 394 | public PulseController setPulsingColor(int pulsingColor) { 395 | this.pulsingColor = pulsingColor; 396 | 397 | return this; 398 | } 399 | 400 | public PulseController setPulsingStrokeWidth(int pulsingStrokeWidth){ 401 | this.pulsingStrokeWidth = pulsingStrokeWidth; 402 | 403 | return this; 404 | } 405 | 406 | /** 407 | * Set a callback to be triggered on (non-canceled or stopped) pulse completions. 408 | *

409 | * This callback is weakly held. 410 | * 411 | * @param finishedListener the callback to be triggered 412 | * @return this instance 413 | */ 414 | public PulseController setFinishedListener(PulseEventListener finishedListener) { 415 | this.finishedListener = new WeakReference(finishedListener); 416 | 417 | return this; 418 | } 419 | 420 | public View getParent() { 421 | return parent.get(); 422 | } 423 | 424 | public long getDurationMs() { 425 | return durationMs; 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/java/com/jackpocket/pulse/PulseTask.java: -------------------------------------------------------------------------------- 1 | package com.jackpocket.pulse; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | 6 | public class PulseTask extends Thread { 7 | 8 | private static final int SLEEP = 15; 9 | 10 | private PulseController controller; 11 | private boolean canceled = false; 12 | 13 | private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); 14 | private Runnable finishedListener; 15 | 16 | public PulseTask(PulseController controller) { 17 | this.controller = controller; 18 | } 19 | 20 | public PulseTask setFinishedListener(Runnable finishedListener) { 21 | this.finishedListener = finishedListener; 22 | 23 | return this; 24 | } 25 | 26 | @Override 27 | public void run() { 28 | try { 29 | final Runnable updateRunnable = new Runnable() { 30 | @Override 31 | public void run(){ 32 | controller.update(); 33 | } 34 | }; 35 | 36 | while (!canceled && controller.isRunning()) { 37 | mainThreadHandler.post(updateRunnable); 38 | 39 | Thread.sleep(SLEEP); 40 | } 41 | } 42 | catch (Exception e) { e.printStackTrace(); } 43 | 44 | mainThreadHandler.post(new Runnable() { 45 | @Override 46 | public void run() { 47 | if (!(canceled || finishedListener == null)) { 48 | finishedListener.run(); 49 | } 50 | 51 | controller = null; 52 | finishedListener = null; 53 | } 54 | }); 55 | } 56 | 57 | public void cancel() { 58 | this.canceled = true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/java/com/jackpocket/pulse/layouts/PulseLayout.java: -------------------------------------------------------------------------------- 1 | package com.jackpocket.pulse.layouts; 2 | 3 | import android.app.Activity; 4 | import android.view.View; 5 | 6 | import com.jackpocket.pulse.PulseController; 7 | 8 | public interface PulseLayout { 9 | 10 | public PulseController attachTo(Activity activity, View view); 11 | 12 | public PulseController getPulseController(); 13 | } 14 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/java/com/jackpocket/pulse/layouts/PulsingLinearLayout.java: -------------------------------------------------------------------------------- 1 | package com.jackpocket.pulse.layouts; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.graphics.Canvas; 7 | import android.os.Build; 8 | import android.util.AttributeSet; 9 | import android.view.View; 10 | import android.widget.LinearLayout; 11 | 12 | import com.jackpocket.pulse.PulseController; 13 | 14 | public class PulsingLinearLayout extends LinearLayout implements PulseLayout { 15 | 16 | private PulseController pulseController = new PulseController(this); 17 | 18 | public PulsingLinearLayout(Context context) { 19 | super(context); 20 | 21 | setWillNotDraw(false); 22 | } 23 | 24 | public PulsingLinearLayout(Context context, AttributeSet attrs) { 25 | super(context, attrs); 26 | 27 | setWillNotDraw(false); 28 | } 29 | 30 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 31 | public PulsingLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { 32 | super(context, attrs, defStyleAttr); 33 | 34 | setWillNotDraw(false); 35 | } 36 | 37 | @Override 38 | protected void dispatchDraw(Canvas canvas) { 39 | super.dispatchDraw(canvas); 40 | 41 | pulseController.draw(canvas); 42 | } 43 | 44 | @Override 45 | public PulseController attachTo(Activity activity, View view){ 46 | return pulseController.attachTo(activity, view); 47 | } 48 | 49 | @Override 50 | public PulseController getPulseController(){ 51 | return pulseController; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/java/com/jackpocket/pulse/layouts/PulsingRelativeLayout.java: -------------------------------------------------------------------------------- 1 | package com.jackpocket.pulse.layouts; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.graphics.Canvas; 6 | import android.util.AttributeSet; 7 | import android.view.View; 8 | import android.widget.RelativeLayout; 9 | 10 | import com.jackpocket.pulse.PulseController; 11 | 12 | public class PulsingRelativeLayout extends RelativeLayout implements PulseLayout { 13 | 14 | private PulseController pulseController = new PulseController(this); 15 | 16 | public PulsingRelativeLayout(Context context) { 17 | super(context); 18 | 19 | setWillNotDraw(false); 20 | } 21 | 22 | public PulsingRelativeLayout(Context context, AttributeSet attrs) { 23 | super(context, attrs); 24 | 25 | setWillNotDraw(false); 26 | } 27 | 28 | public PulsingRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { 29 | super(context, attrs, defStyleAttr); 30 | 31 | setWillNotDraw(false); 32 | } 33 | 34 | @Override 35 | protected void dispatchDraw(Canvas canvas) { 36 | super.dispatchDraw(canvas); 37 | 38 | pulseController.draw(canvas); 39 | } 40 | 41 | @Override 42 | public PulseController attachTo(Activity activity, View view){ 43 | return pulseController.attachTo(activity, view); 44 | } 45 | 46 | @Override 47 | public PulseController getPulseController(){ 48 | return pulseController; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/java/com/jackpocket/pulse/layouts/PulsingView.java: -------------------------------------------------------------------------------- 1 | package com.jackpocket.pulse.layouts; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.graphics.Canvas; 7 | import android.os.Build; 8 | import android.util.AttributeSet; 9 | import android.view.View; 10 | 11 | import com.jackpocket.pulse.PulseController; 12 | 13 | public class PulsingView extends View implements PulseLayout { 14 | 15 | private PulseController pulseController = new PulseController(this); 16 | 17 | public PulsingView(Context context) { 18 | super(context); 19 | setWillNotDraw(false); 20 | } 21 | 22 | public PulsingView(Context context, AttributeSet attrs) { 23 | super(context, attrs); 24 | setWillNotDraw(false); 25 | } 26 | 27 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 28 | public PulsingView(Context context, AttributeSet attrs, int defStyleAttr) { 29 | super(context, attrs, defStyleAttr); 30 | setWillNotDraw(false); 31 | } 32 | 33 | @Override 34 | protected void dispatchDraw(Canvas canvas) { 35 | super.dispatchDraw(canvas); 36 | 37 | pulseController.draw(canvas); 38 | } 39 | 40 | @Override 41 | public PulseController attachTo(Activity activity, View view){ 42 | return pulseController.attachTo(activity, view); 43 | } 44 | 45 | @Override 46 | public PulseController getPulseController(){ 47 | return pulseController; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/res/values/booleans.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #FFFFFF 5 | 6 | 7 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/res/values/integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 300 4 | 5 | 1500 6 | 850 7 | 300 8 | 9 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pulse-indicator/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pulse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackpocket/android-pulse-indicator/a2a8363ded4699ad22e1fd6964111c17d44fe75f/pulse.gif -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # pulse-indicator 2 | 3 | [![Download](https://img.shields.io/maven-central/v/com.jackpocket/pulse-indicator)](https://search.maven.org/artifact/com.jackpocket/pulse-indicator) 4 | 5 | An Android system for indicating Views with fading pulses 6 | 7 | ![pulse-indicator Sample](https://github.com/jackpocket/android-pulse-indicator/raw/master/pulse.gif) 8 | 9 | # Installation 10 | 11 | ``` 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | dependencies { 17 | compile('com.jackpocket:pulse-indicator:2.0.0') 18 | } 19 | ``` 20 | 21 | # Usage 22 | 23 | ##### Layout Approach 24 | 25 | Make the root of your `Activity`'s layout one of the base `PulseLayouts` included in this library: the `PulsingLinearLayout` or the `PulsingRelativeLayout`. e.g. 26 | 27 | ```xml 28 | 31 | ``` 32 | 33 | Then find it in your Activity and simply call `PulseLayout.attachTo(Activity, View)`. Done. e.g. 34 | 35 | ```java 36 | ((PulseLayout) findViewById(R.id.my_pulsing_layout)) 37 | .attachTo(this, findViewById(R.id.some_view_I_want_to_indicate); 38 | ``` 39 | 40 | ##### PulseView Approach 41 | 42 | Just add a `PulseView` to your layout (make sure you're using a ViewGroup that allows overlapping children (e.g. RelativeLayout)). 43 | 44 | ```xml 45 | 46 | 50 | 51 | ``` 52 | 53 | You could then use it the same way you would going the Layout Approach mentioned above. 54 | 55 | ##### Custom Approach 56 | 57 | If you want to add pulsing to your own custom layouts, just checkout one of the supplied layout class files for detailed information on how to implemented the `PulsingController` manually. 58 | 59 | Changing the values at runtime can also be configured by working with the PulseController: 60 | 61 | ```java 62 | ((PulseLayout) findViewById(R.id.my_pulsing_layout)) 63 | .getPulseController() 64 | .setCirclePathOverride(false) // Set it to use the rectangular boundaries instead of circle pulsing 65 | .setPulsingColor(0xFF22FF22) // Set the pulse starting color 66 | .setPulsingStrokeWidth(10) // Override the dynamic stroke width with a custom one 67 | .setDurationMs(1500) // Set the overall duration of the pulsing (will continue until no pulses exist) 68 | .setPulseLifeSpanMs(900) // The length of time a pulse is visible 69 | .setRespawnRateMs(300) // The rate at which a new pulse should be added 70 | .setAlphaInterpolator(new AccelerateInterpolator()) // Set the Interpolator for the alpha animation 71 | .setScaleInterpolator(new LinearInterpolator()) // Set the Interpolator for the scaling animation 72 | 73 | // Set a callback to be triggered when the pulsing finished for a View. 74 | // Calling attach() or stopPulsing() before it completes will prevent it from being triggered. 75 | // The supplied PulseEventListener is weakly held by the PulseController. 76 | .setFinishedListener(this) 77 | 78 | // Attach the configured controller to the target and start pulsing 79 | .attachTo(this, findViewById(R.id.some_view_I_want_to_indicate)); 80 | ``` 81 | 82 | ### Configs 83 | 84 | The default configs for pulsing color, duration, individual lifespan, respawn rates can be overwritten via the following, respectively: 85 | 86 | R.color.pulse__color 87 | R.integer.pulse__duration_default 88 | R.integer.pulse__lifespan_default 89 | R.integer.pulse__respawn_rate_default 90 | 91 | Note, all time values are in milliseconds. 92 | 93 | ### Moved to MavenCentral 94 | 95 | As of version 2.0.0, pulse-indicator will be hosted on MavenCentral. Versions 1.1.0 and below will remain on JCenter. 96 | 97 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':pulse-indicator', ':pulse-indicator-sample' 2 | --------------------------------------------------------------------------------