├── LICENSE ├── README.md ├── Rumble.java └── Rumble.kt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joseph Meir Rubin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rumble-4-Android 2 | r4a abstracts away the annoying Android Vibrator API which suffers from a number of issues: 3 | 4 | - Different versions of android have **deprecated** or **added** different functions and you are expected to know which ones to use depending on the device version. 5 | - You have to make a **new object** for every single vibration you want to run. 6 | - Patterns are tedious and **not intuitive**. 7 | 8 | Instead... 9 | 10 | ## Setup *(easy!)* 11 | 12 | 1. Download ```Rumble.java``` and place it anywhere in the **source directory** of your project. 13 | 2. Add the line `````` inside the ```manifest``` tag of your project's ```AndroidManifest.xml```. 14 | 3. Call ```Rumble.init(applicationContext)``` and pass it your application's **Context**. For example, you can place the following code in your activity: 15 | >MainActivity.java 16 | 17 | ```java 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | Rumble.init(getApplicationContext()); 22 | } 23 | ``` 24 | 25 | ## Use *(fluent!)* 26 | **One-time** device vibration 27 | ```java Rumble.once(500); // Vibrate for 500 milliseconds.``` 28 | 29 | **Patterns** 30 | ```java 31 | Rumble.makePattern() 32 | .beat(300) 33 | .rest(250) 34 | .beat(720) 35 | .playPattern(); 36 | ``` 37 | **Repeating patterns** 38 | ```java 39 | Rumble.makePattern() 40 | .rest(200) 41 | .beat(30) 42 | .beat(holdDuration) // Automatically adds to previous beat. 43 | .playPattern(4); // Play 4 times in a row. 44 | ``` 45 | **Save patterns for later** 46 | ```java 47 | RumblePattern pattern = Rumble.makePattern() 48 | .beat(30).rest(150).beat(40).rest(40); 49 | 50 | pattern.rest(80).beat(700); // Add to a pattern later. 51 | pattern.playPattern(); // Play it whenever you like. 52 | ``` 53 | 54 | **Lock patterns to prevent mutation** 55 | ```java 56 | pattern.lock(); 57 | pattern.playPattern(3); // Works just fine. 58 | pattern.beat(500).rest(250) // Throws IllegalStateException. 59 | ``` 60 | -------------------------------------------------------------------------------- /Rumble.java: -------------------------------------------------------------------------------- 1 | package box.shoe.gameutils.rumble; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.os.VibrationEffect; 6 | import android.os.Vibrator; 7 | import android.util.Log; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | import box.gift.gameutils.BuildConfig; 14 | 15 | public class Rumble 16 | { 17 | private static Vibrator vibrator; 18 | private static boolean rumbleDisabled; 19 | 20 | public static void init(Context applicationContext) 21 | { 22 | vibrator = (Vibrator) applicationContext.getSystemService(Context.VIBRATOR_SERVICE); 23 | rumbleDisabled = (vibrator == null || !vibrator.hasVibrator()); 24 | if (rumbleDisabled && BuildConfig.DEBUG) 25 | { 26 | Log.w("Rumble", "System does not have a Vibrator, or the permission is disabled. " + 27 | "Rumble has been turned rest. Subsequent calls to static methods will have no effect."); 28 | } 29 | } 30 | 31 | private static void apiIndependentVibrate(long milliseconds) 32 | { 33 | if (rumbleDisabled) 34 | { 35 | return; 36 | } 37 | 38 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) 39 | { 40 | vibrator.vibrate(VibrationEffect.createOneShot(milliseconds, VibrationEffect.DEFAULT_AMPLITUDE)); 41 | } 42 | else 43 | { 44 | vibrator.vibrate(milliseconds); 45 | } 46 | } 47 | 48 | private static void apiIndependentVibrate(long[] pattern) 49 | { 50 | if (rumbleDisabled) 51 | { 52 | return; 53 | } 54 | 55 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) 56 | { 57 | vibrator.vibrate(VibrationEffect.createWaveform(pattern, -1)); 58 | } 59 | else 60 | { 61 | vibrator.vibrate(pattern, -1); 62 | } 63 | } 64 | 65 | public static void stop() 66 | { 67 | if (rumbleDisabled) 68 | { 69 | return; 70 | } 71 | 72 | vibrator.cancel(); 73 | } 74 | 75 | public static void once(long milliseconds) 76 | { 77 | apiIndependentVibrate(milliseconds); 78 | } 79 | 80 | public static RumblePattern makePattern() 81 | { 82 | return new RumblePattern(); 83 | } 84 | 85 | public static class RumblePattern 86 | { 87 | private List internalPattern; 88 | private boolean locked; 89 | 90 | private RumblePattern() 91 | { 92 | locked = false; 93 | internalPattern = new ArrayList<>(); 94 | internalPattern.add(0L); 95 | } 96 | 97 | public RumblePattern beat(long milliseconds) 98 | { 99 | if (locked) 100 | { 101 | throw new IllegalStateException("RumblePattern is locked! Cannot modify its state."); 102 | } 103 | 104 | if (internalPattern.size() % 2 == 0) 105 | { 106 | internalPattern.set(internalPattern.size() - 1, internalPattern.get(internalPattern.size() - 1) + milliseconds); 107 | } 108 | else 109 | { 110 | internalPattern.add(milliseconds); 111 | } 112 | return this; 113 | } 114 | 115 | public RumblePattern rest(long milliseconds) 116 | { 117 | if (locked) 118 | { 119 | throw new IllegalStateException("RumblePattern is locked! Cannot modify its state."); 120 | } 121 | 122 | if (internalPattern.size() % 2 == 0) 123 | { 124 | internalPattern.add(milliseconds); 125 | } 126 | else 127 | { 128 | internalPattern.set(internalPattern.size() - 1, internalPattern.get(internalPattern.size() - 1) + milliseconds); 129 | } 130 | return this; 131 | } 132 | 133 | public void lock() 134 | { 135 | if (locked) 136 | { 137 | throw new IllegalStateException("RumblePattern is already locked! Use isLocked() to check."); 138 | } 139 | locked = true; 140 | } 141 | 142 | public boolean isLocked() 143 | { 144 | return locked; 145 | } 146 | 147 | public void playPattern() 148 | { 149 | playPattern(1); 150 | } 151 | 152 | public void playPattern(int numberOfTimes) 153 | { 154 | if (numberOfTimes < 0) 155 | { 156 | throw new IllegalArgumentException("numberOfTimes must be >= 0"); 157 | } 158 | 159 | boolean endsWithRest = internalPattern.size() % 2 == 0; 160 | 161 | // We have a List but we need a long[]. We can't simply use toArray because that yields a Long[]. 162 | // Reserve enough space to hold the full pattern as many times as necessary to play the pattern the right number of times. 163 | long[] primitiveArray = new long[internalPattern.size() * numberOfTimes - (endsWithRest ? 0 : numberOfTimes - 1)]; 164 | for (int i = 0; i < internalPattern.size(); i++) 165 | { 166 | // Auto unboxing converts each Long to a long. 167 | primitiveArray[i] = internalPattern.get(i); 168 | } 169 | 170 | // Copy the array into itself to duplicate the pattern enough times. 171 | // Not a simple copy - we must overlay the copies if the pattern ends in a rest. 172 | // R B R 173 | // [100, 300, 500] 174 | // + 175 | // [100, 300, 500] 176 | for (int i = 1; i < numberOfTimes; i++) 177 | { 178 | for (int j = 0; j < internalPattern.size(); j++) 179 | { 180 | int k = j + (internalPattern.size() * i) - (endsWithRest ? 0 : i); 181 | primitiveArray[k] += primitiveArray[j]; 182 | } 183 | } 184 | 185 | apiIndependentVibrate(primitiveArray); 186 | } 187 | 188 | @Override 189 | public String toString() 190 | { 191 | return "RumblePattern{" + 192 | "internalPattern=" + internalPattern + 193 | '}'; 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /Rumble.kt: -------------------------------------------------------------------------------- 1 | // TODO: Change this 2 | package box.shoe.gameutils.rumble; 3 | 4 | import android.content.Context 5 | import android.os.Build 6 | import android.os.VibrationEffect 7 | import android.os.Vibrator 8 | import android.util.Log 9 | 10 | // TODO: Change this 11 | import box.gift.gameutils.BuildConfig; 12 | 13 | import java.util.ArrayList 14 | import java.util.Arrays 15 | 16 | object Rumble { 17 | private var vibrator: Vibrator? = null 18 | private var rumbleDisabled: Boolean = false 19 | 20 | fun init(applicationContext: Context) { 21 | vibrator = applicationContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator 22 | rumbleDisabled = vibrator == null || !vibrator!!.hasVibrator() 23 | if (rumbleDisabled && BuildConfig.DEBUG) { 24 | Log.w("Rumble", "System does not have a Vibrator, or the permission is disabled. " + "Rumble has been turned rest. Subsequent calls to static methods will have no effect.") 25 | } 26 | } 27 | 28 | private fun apiIndependentVibrate(milliseconds: Long) { 29 | if (rumbleDisabled) { 30 | return 31 | } 32 | 33 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 34 | vibrator!!.vibrate(VibrationEffect.createOneShot(milliseconds, VibrationEffect.DEFAULT_AMPLITUDE)) 35 | } else { 36 | vibrator!!.vibrate(milliseconds) 37 | } 38 | } 39 | 40 | private fun apiIndependentVibrate(pattern: LongArray) { 41 | if (rumbleDisabled) { 42 | return 43 | } 44 | 45 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 46 | vibrator!!.vibrate(VibrationEffect.createWaveform(pattern, -1)) 47 | } else { 48 | vibrator!!.vibrate(pattern, -1) 49 | } 50 | } 51 | 52 | fun stop() { 53 | if (rumbleDisabled) { 54 | return 55 | } 56 | 57 | vibrator!!.cancel() 58 | } 59 | 60 | fun once(milliseconds: Long) { 61 | apiIndependentVibrate(milliseconds) 62 | } 63 | 64 | fun makePattern(): RumblePattern { 65 | return RumblePattern() 66 | } 67 | 68 | class RumblePattern internal constructor() { 69 | private val internalPattern: MutableList 70 | var isLocked: Boolean = false 71 | private set 72 | 73 | init { 74 | isLocked = false 75 | internalPattern = ArrayList() 76 | internalPattern.add(0L) 77 | } 78 | 79 | fun beat(milliseconds: Long): RumblePattern { 80 | if (isLocked) { 81 | throw IllegalStateException("RumblePattern is locked! Cannot modify its state.") 82 | } 83 | 84 | if (internalPattern.size % 2 == 0) { 85 | internalPattern[internalPattern.size - 1] = internalPattern[internalPattern.size - 1] + milliseconds 86 | } else { 87 | internalPattern.add(milliseconds) 88 | } 89 | return this 90 | } 91 | 92 | fun rest(milliseconds: Long): RumblePattern { 93 | if (isLocked) { 94 | throw IllegalStateException("RumblePattern is locked! Cannot modify its state.") 95 | } 96 | 97 | if (internalPattern.size % 2 == 0) { 98 | internalPattern.add(milliseconds) 99 | } else { 100 | internalPattern[internalPattern.size - 1] = internalPattern[internalPattern.size - 1] + milliseconds 101 | } 102 | return this 103 | } 104 | 105 | fun lock() { 106 | if (isLocked) { 107 | throw IllegalStateException("RumblePattern is already locked! Use isLocked() to check.") 108 | } 109 | isLocked = true 110 | } 111 | 112 | @JvmOverloads 113 | fun playPattern(numberOfTimes: Int = 1) { 114 | if (numberOfTimes < 0) { 115 | throw IllegalArgumentException("numberOfTimes must be >= 0") 116 | } 117 | 118 | val endsWithRest = internalPattern.size % 2 == 0 119 | 120 | // We have a List but we need a long[]. We can't simply use toArray because that yields a Long[]. 121 | // Reserve enough space to hold the full pattern as many times as necessary to play the pattern the right number of times. 122 | val primitiveArray = LongArray(internalPattern.size * numberOfTimes - if (endsWithRest) 0 else numberOfTimes - 1) 123 | for (i in internalPattern.indices) { 124 | // Auto unboxing converts each Long to a long. 125 | primitiveArray[i] = internalPattern[i] 126 | } 127 | 128 | // Copy the array into itself to duplicate the pattern enough times. 129 | // Not a simple copy - we must overlay the copies if the pattern ends in a rest. 130 | // R B R 131 | // [100, 300, 500] 132 | // + 133 | // [100, 300, 500] 134 | for (i in 1 until numberOfTimes) { 135 | for (j in internalPattern.indices) { 136 | val k = j + internalPattern.size * i - if (endsWithRest) 0 else i 137 | primitiveArray[k] += primitiveArray[j] 138 | } 139 | } 140 | 141 | apiIndependentVibrate(primitiveArray) 142 | } 143 | 144 | override fun toString(): String { 145 | return "RumblePattern{" + 146 | "internalPattern=" + internalPattern + 147 | "}" 148 | } 149 | } 150 | } 151 | --------------------------------------------------------------------------------